Initial Commit
This commit is contained in:
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
node_modules/
|
||||
dist/
|
||||
logs/
|
||||
*.log
|
||||
.env
|
||||
.DS_Store
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
186
README.md
Normal file
186
README.md
Normal file
@ -0,0 +1,186 @@
|
||||
# SVC-HCM-CRAWLER
|
||||
|
||||
Hikvision Device Crawling Service for HCM Bridge
|
||||
|
||||
This service crawls Hikvision access control devices and sends events to the `svc-hcmbridge` service.
|
||||
|
||||
## Features
|
||||
|
||||
- Automatically crawls Hikvision devices configured for crawling (flag=1 or iscrawling=1)
|
||||
- Fetches device MAC address from device info endpoint
|
||||
- Handles HTTP Digest authentication
|
||||
- Duplicate event detection
|
||||
- Sends events to `svc-hcmbridge` at `/api/dooraccess/logs`
|
||||
- PM2 deployment ready
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js 20+
|
||||
- MySQL database (same as svc-hcmbridge)
|
||||
- PM2 (for deployment)
|
||||
- `entity` package (from parent directory)
|
||||
|
||||
## Installation
|
||||
|
||||
1. Install dependencies:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
2. Link the entity package:
|
||||
```bash
|
||||
cd ../entity
|
||||
npm install
|
||||
cd ../svc-hcm-crawler
|
||||
```
|
||||
|
||||
3. Build the project:
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Edit `config/default.json` or `config/prod.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"database": {
|
||||
"hcm": {
|
||||
"host": "127.0.0.1",
|
||||
"port": "3306",
|
||||
"username": "root",
|
||||
"password": "your_password",
|
||||
"database": "tt_hcm"
|
||||
}
|
||||
},
|
||||
"bridge": {
|
||||
"host": "127.0.0.1",
|
||||
"port": "3000",
|
||||
"endpoint": "/api/dooraccess/logs"
|
||||
},
|
||||
"hikvision": {
|
||||
"defaultUsername": "admin",
|
||||
"defaultPassword": "Passwordhik_1",
|
||||
"defaultPort": 80
|
||||
},
|
||||
"crawler": {
|
||||
"interval": "*/1 * * * *"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
Run in development mode:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Deployment with PM2
|
||||
|
||||
1. Build the project:
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
2. Start with PM2:
|
||||
```bash
|
||||
npm run pm2:start
|
||||
```
|
||||
|
||||
Or manually:
|
||||
```bash
|
||||
pm2 start ecosystem.config.js
|
||||
```
|
||||
|
||||
3. Check status:
|
||||
```bash
|
||||
pm2 list
|
||||
pm2 logs svc-hcm-crawler
|
||||
```
|
||||
|
||||
4. Stop the service:
|
||||
```bash
|
||||
npm run pm2:stop
|
||||
# or
|
||||
pm2 stop svc-hcm-crawler
|
||||
```
|
||||
|
||||
5. Restart the service:
|
||||
```bash
|
||||
npm run pm2:restart
|
||||
# or
|
||||
pm2 restart svc-hcm-crawler
|
||||
```
|
||||
|
||||
6. Delete from PM2:
|
||||
```bash
|
||||
npm run pm2:delete
|
||||
# or
|
||||
pm2 delete svc-hcm-crawler
|
||||
```
|
||||
|
||||
## PM2 Commands
|
||||
|
||||
- `npm run pm2:start` - Start the service
|
||||
- `npm run pm2:stop` - Stop the service
|
||||
- `npm run pm2:restart` - Restart the service
|
||||
- `npm run pm2:delete` - Delete from PM2
|
||||
- `npm run pm2:logs` - View logs
|
||||
|
||||
## How It Works
|
||||
|
||||
1. The service runs on a cron schedule (default: every 1 minute)
|
||||
2. It queries `tbl_deviceinfo` for devices with:
|
||||
- `brand = 'HIKVISION'`
|
||||
- `flag = 1` OR `iscrawling = 1`
|
||||
- `isdeleted = 0`
|
||||
3. For each device:
|
||||
- Fetches MAC address from device info endpoint
|
||||
- Makes authenticated requests to `/ISAPI/AccessControl/AcsEvent`
|
||||
- Maps events to webhook format
|
||||
- Checks for duplicates
|
||||
- Sends events to `svc-hcmbridge` at `/api/dooraccess/logs`
|
||||
|
||||
## Logs
|
||||
|
||||
Logs are stored in:
|
||||
- PM2 logs: `./logs/pm2-out.log` and `./logs/pm2-error.log`
|
||||
- Console output (when running with `npm start`)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
1. **Database connection issues:**
|
||||
- Check database credentials in config
|
||||
- Ensure database is accessible
|
||||
- Check if `entity` package is properly linked
|
||||
|
||||
2. **Bridge service not responding:**
|
||||
- Ensure `svc-hcmbridge` is running
|
||||
- Check bridge host/port in config
|
||||
- Verify endpoint path is correct
|
||||
|
||||
3. **Device connection issues:**
|
||||
- Check device IP addresses in `tbl_deviceinfo`
|
||||
- Verify device credentials
|
||||
- Check network connectivity to devices
|
||||
|
||||
4. **MAC address not being saved:**
|
||||
- Check logs for XML parsing errors
|
||||
- Verify device info endpoint is accessible
|
||||
- Check device returns valid XML
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Set `NODE_ENV` to `prod` for production:
|
||||
```bash
|
||||
export NODE_ENV=prod
|
||||
```
|
||||
|
||||
Or use PM2 ecosystem config which sets it automatically.
|
||||
|
||||
## License
|
||||
|
||||
ISC
|
||||
|
||||
37
config/default.json
Normal file
37
config/default.json
Normal file
@ -0,0 +1,37 @@
|
||||
{
|
||||
"app": {
|
||||
"name": "SVC-HCM-CRAWLER",
|
||||
"description": "Hikvision Device Crawling Service",
|
||||
"version": "1.0.0",
|
||||
"env": "Development"
|
||||
},
|
||||
"crawler": {
|
||||
"interval": "*/1 * * * *",
|
||||
"maxResults": 10,
|
||||
"timeout": 10000
|
||||
},
|
||||
"database": {
|
||||
"hcm": {
|
||||
"engine": "mysql",
|
||||
"host": "127.0.0.1",
|
||||
"port": "3306",
|
||||
"username": "root",
|
||||
"password": "r00t@dm1n05",
|
||||
"database": "tt_hcm",
|
||||
"logging": false,
|
||||
"sync": false
|
||||
}
|
||||
},
|
||||
"bridge": {
|
||||
"host": "127.0.0.1",
|
||||
"port": "3000",
|
||||
"endpoint": "/api/dooraccess/logs",
|
||||
"timeout": 30000
|
||||
},
|
||||
"hikvision": {
|
||||
"defaultUsername": "admin",
|
||||
"defaultPassword": "Passwordhik_1",
|
||||
"defaultPort": 80
|
||||
}
|
||||
}
|
||||
|
||||
37
config/prod.json
Normal file
37
config/prod.json
Normal file
@ -0,0 +1,37 @@
|
||||
{
|
||||
"app": {
|
||||
"name": "SVC-HCM-CRAWLER",
|
||||
"description": "Hikvision Device Crawling Service",
|
||||
"version": "1.0.0",
|
||||
"env": "Production"
|
||||
},
|
||||
"crawler": {
|
||||
"interval": "*/1 * * * *",
|
||||
"maxResults": 10,
|
||||
"timeout": 10000
|
||||
},
|
||||
"database": {
|
||||
"hcm": {
|
||||
"engine": "mysql",
|
||||
"host": "127.0.0.1",
|
||||
"port": "3306",
|
||||
"username": "apphcm",
|
||||
"password": "$ppHCMTT#2024",
|
||||
"database": "dbhcm",
|
||||
"logging": false,
|
||||
"sync": false
|
||||
}
|
||||
},
|
||||
"bridge": {
|
||||
"host": "127.0.0.1",
|
||||
"port": "3000",
|
||||
"endpoint": "/api/dooraccess/logs",
|
||||
"timeout": 30000
|
||||
},
|
||||
"hikvision": {
|
||||
"defaultUsername": "admin",
|
||||
"defaultPassword": "Passwordhik_1",
|
||||
"defaultPort": 80
|
||||
}
|
||||
}
|
||||
|
||||
28
ecosystem.config.js
Normal file
28
ecosystem.config.js
Normal file
@ -0,0 +1,28 @@
|
||||
module.exports = {
|
||||
apps: [
|
||||
{
|
||||
name: "svc-hcm-crawler",
|
||||
script: "dist/src/index.js",
|
||||
instances: 1,
|
||||
exec_mode: "fork",
|
||||
watch: false,
|
||||
max_memory_restart: "500M",
|
||||
error_file: "./logs/pm2-error.log",
|
||||
out_file: "./logs/pm2-out.log",
|
||||
log_date_format: "YYYY-MM-DD HH:mm:ss Z",
|
||||
merge_logs: true,
|
||||
env: {
|
||||
NODE_ENV: "default",
|
||||
TZ: "Asia/Jakarta"
|
||||
},
|
||||
env_production: {
|
||||
NODE_ENV: "prod",
|
||||
TZ: "Asia/Jakarta"
|
||||
},
|
||||
autorestart: true,
|
||||
max_restarts: 10,
|
||||
min_uptime: "10s"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
11
nodemon.json
Normal file
11
nodemon.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"watch": ["src"],
|
||||
"ext": "ts",
|
||||
"ignore": ["src/**/*.spec.ts", "node_modules"],
|
||||
"exec": "ts-node src/index.ts",
|
||||
"env": {
|
||||
"NODE_ENV": "default",
|
||||
"TZ": "Asia/Jakarta"
|
||||
}
|
||||
}
|
||||
|
||||
2314
package-lock.json
generated
Normal file
2314
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
45
package.json
Normal file
45
package.json
Normal file
@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "svc-hcm-crawler",
|
||||
"version": "1.0.0",
|
||||
"description": "Hikvision Device Crawling Service for HCM Bridge",
|
||||
"main": "dist/src/index.js",
|
||||
"scripts": {
|
||||
"start": "TZ='Asia/Jakarta' node dist/src/index.js",
|
||||
"dev": "nodemon",
|
||||
"build": "tsc",
|
||||
"pm2:start": "pm2 start ecosystem.config.js",
|
||||
"pm2:stop": "pm2 stop svc-hcm-crawler",
|
||||
"pm2:restart": "pm2 restart svc-hcm-crawler",
|
||||
"pm2:delete": "pm2 delete svc-hcm-crawler",
|
||||
"pm2:logs": "pm2 logs svc-hcm-crawler"
|
||||
},
|
||||
"keywords": [
|
||||
"hikvision",
|
||||
"crawler",
|
||||
"hcm"
|
||||
],
|
||||
"author": "STS",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@types/xml2js": "^0.4.14",
|
||||
"axios": "^1.7.8",
|
||||
"config": "^3.3.12",
|
||||
"entity": "file:../entity",
|
||||
"moment-timezone": "~0.5.46",
|
||||
"mysql2": "^3.12.0",
|
||||
"node-cron": "^4.1.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"tslog": "^4.9.3",
|
||||
"typeorm": "^0.3.28",
|
||||
"typescript": "^5.9.3",
|
||||
"uuid": "^11.0.3",
|
||||
"xml2js": "^0.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"@types/node-cron": "^3.0.11",
|
||||
"nodemon": "^3.0.0",
|
||||
"ts-node": "^10.9.0"
|
||||
}
|
||||
}
|
||||
|
||||
44
src/helpers/orm.ts
Normal file
44
src/helpers/orm.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import config from "config";
|
||||
import { DataSource } from "typeorm";
|
||||
import { entities } from "entity";
|
||||
import { ILogObj, Logger } from "tslog";
|
||||
|
||||
export class OrmHelper {
|
||||
static DB: DataSource = null;
|
||||
|
||||
static async setup(): Promise<void> {
|
||||
const log: Logger<ILogObj> = new Logger({
|
||||
name: "[OrmHelper]",
|
||||
type: "pretty",
|
||||
});
|
||||
log.settings.prettyLogTimeZone = "local";
|
||||
|
||||
const engine: "mysql" = config.get("database.hcm.engine");
|
||||
|
||||
OrmHelper.DB = new DataSource({
|
||||
type: engine,
|
||||
host: config.get("database.hcm.host"),
|
||||
port: Number(config.get("database.hcm.port")),
|
||||
username: String(config.get("database.hcm.username")),
|
||||
password: String(config.get("database.hcm.password")),
|
||||
database: String(config.get("database.hcm.database")),
|
||||
synchronize: config.get("database.hcm.sync"),
|
||||
logging: config.get("database.hcm.logging"),
|
||||
entities: entities,
|
||||
subscribers: [],
|
||||
migrations: [],
|
||||
extra: {
|
||||
query: "SET TIMEZONE='Asia/Jakarta';",
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
await OrmHelper.DB.initialize();
|
||||
log.info("Database initialized successfully");
|
||||
} catch (error: any) {
|
||||
log.error("Database initialization failed:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
109
src/index.ts
Normal file
109
src/index.ts
Normal file
@ -0,0 +1,109 @@
|
||||
import config from "config";
|
||||
import moment from "moment-timezone";
|
||||
import { ILogObj, Logger } from "tslog";
|
||||
import cron from "node-cron";
|
||||
import { OrmHelper } from "./helpers/orm";
|
||||
import { CrawlingService } from "./services/crawling";
|
||||
|
||||
// Set timezone
|
||||
moment.tz.setDefault("Asia/Jakarta");
|
||||
|
||||
const log: Logger<ILogObj> = new Logger({
|
||||
name: "[CrawlerIndex]",
|
||||
type: "pretty",
|
||||
});
|
||||
log.settings.prettyLogTimeZone = "local";
|
||||
|
||||
let isCrawlingRunning = false;
|
||||
|
||||
/**
|
||||
* Start the crawling service
|
||||
*/
|
||||
async function startCrawlingService() {
|
||||
try {
|
||||
// Initialize database connection
|
||||
log.info("Initializing database connection...");
|
||||
await OrmHelper.setup();
|
||||
|
||||
// Wait a bit for database to be ready
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
|
||||
// Verify database is initialized
|
||||
if (!OrmHelper.DB || !OrmHelper.DB.isInitialized) {
|
||||
throw new Error("Database not initialized");
|
||||
}
|
||||
|
||||
log.info("Database connection established");
|
||||
|
||||
// Start cron job
|
||||
const cronInterval = config.get("crawler.interval");
|
||||
log.info(`Starting crawling service with interval: ${cronInterval}`);
|
||||
|
||||
const crawlingJob = cron.schedule(cronInterval, async () => {
|
||||
if (isCrawlingRunning) {
|
||||
log.warn("Crawling service is still running. Skipping this schedule.");
|
||||
return;
|
||||
}
|
||||
|
||||
log.info(`Running crawling service at: ${new Date().toLocaleString()}`);
|
||||
isCrawlingRunning = true;
|
||||
|
||||
try {
|
||||
await CrawlingService.runCrawling();
|
||||
log.info(`Crawling service completed at: ${new Date().toLocaleString()}`);
|
||||
} catch (error) {
|
||||
log.error("Crawling service failed:", error);
|
||||
} finally {
|
||||
isCrawlingRunning = false;
|
||||
}
|
||||
});
|
||||
|
||||
log.info(
|
||||
`Crawling service started. App: ${config.get("app.name")} v${config.get("app.version")}`
|
||||
);
|
||||
log.info(`Environment: ${config.get("app.env")}`);
|
||||
log.info(`Cron schedule: ${cronInterval}`);
|
||||
|
||||
// Run immediately on startup (optional)
|
||||
// Uncomment the line below if you want to run crawling immediately on startup
|
||||
// await CrawlingService.runCrawling();
|
||||
} catch (error) {
|
||||
log.error("Failed to start crawling service:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle graceful shutdown
|
||||
const onCloseSignal = () => {
|
||||
log.info("SIGINT/SIGTERM received, shutting down gracefully");
|
||||
if (OrmHelper.DB && OrmHelper.DB.isInitialized) {
|
||||
OrmHelper.DB.destroy()
|
||||
.then(() => {
|
||||
log.info("Database connection closed");
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
log.error("Error closing database connection:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
} else {
|
||||
process.exit(0);
|
||||
}
|
||||
};
|
||||
|
||||
process.on("SIGINT", onCloseSignal);
|
||||
process.on("SIGTERM", onCloseSignal);
|
||||
|
||||
// Handle uncaught errors
|
||||
process.on("unhandledRejection", (reason, promise) => {
|
||||
log.error("Unhandled Rejection at:", promise, "reason:", reason);
|
||||
});
|
||||
|
||||
process.on("uncaughtException", (error) => {
|
||||
log.error("Uncaught Exception:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Start the service
|
||||
startCrawlingService();
|
||||
|
||||
1196
src/services/crawling.ts
Normal file
1196
src/services/crawling.ts
Normal file
File diff suppressed because it is too large
Load Diff
26
tsconfig.json
Normal file
26
tsconfig.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"module": "commonjs",
|
||||
"lib": ["es6", "es2017", "esnext.asynciterable"],
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./",
|
||||
"moduleResolution": "node",
|
||||
"removeComments": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"resolveJsonModule": true,
|
||||
"typeRoots": ["./node_modules/@types"],
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["./src/**/*.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user