heathrow 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +58 -0
- data/README.md +205 -0
- data/bin/heathrow +42 -0
- data/bin/heathrowd +283 -0
- data/docs/ARCHITECTURE.md +1172 -0
- data/docs/DATABASE_SCHEMA.md +685 -0
- data/docs/DEVELOPMENT_WORKFLOW.md +867 -0
- data/docs/DISCORD_SETUP.md +142 -0
- data/docs/GMAIL_OAUTH_SETUP.md +120 -0
- data/docs/PLUGIN_SYSTEM.md +1370 -0
- data/docs/PROJECT_PLAN.md +1022 -0
- data/docs/README.md +417 -0
- data/docs/REDDIT_SETUP.md +174 -0
- data/docs/REPLY_FORWARD.md +182 -0
- data/docs/WHATSAPP_TELEGRAM_SETUP.md +306 -0
- data/heathrow.gemspec +34 -0
- data/heathrowd.service +21 -0
- data/img/heathrow.svg +95 -0
- data/img/rss_threaded.png +0 -0
- data/img/sources.png +0 -0
- data/lib/heathrow/address_book.rb +42 -0
- data/lib/heathrow/config.rb +332 -0
- data/lib/heathrow/database.rb +731 -0
- data/lib/heathrow/database_new.rb +392 -0
- data/lib/heathrow/event_bus.rb +175 -0
- data/lib/heathrow/logger.rb +122 -0
- data/lib/heathrow/message.rb +176 -0
- data/lib/heathrow/message_composer.rb +399 -0
- data/lib/heathrow/message_organizer.rb +774 -0
- data/lib/heathrow/migrations/001_initial_schema.rb +248 -0
- data/lib/heathrow/notmuch.rb +45 -0
- data/lib/heathrow/oauth2_smtp.rb +254 -0
- data/lib/heathrow/plugin/base.rb +212 -0
- data/lib/heathrow/plugin_manager.rb +141 -0
- data/lib/heathrow/poller.rb +93 -0
- data/lib/heathrow/smtp_sender.rb +204 -0
- data/lib/heathrow/source.rb +39 -0
- data/lib/heathrow/sources/base.rb +74 -0
- data/lib/heathrow/sources/discord.rb +357 -0
- data/lib/heathrow/sources/gmail.rb +294 -0
- data/lib/heathrow/sources/imap.rb +198 -0
- data/lib/heathrow/sources/instagram.rb +307 -0
- data/lib/heathrow/sources/instagram_fetch.py +101 -0
- data/lib/heathrow/sources/instagram_send.py +55 -0
- data/lib/heathrow/sources/instagram_send_marionette.py +104 -0
- data/lib/heathrow/sources/maildir.rb +606 -0
- data/lib/heathrow/sources/messenger.rb +212 -0
- data/lib/heathrow/sources/messenger_fetch.js +297 -0
- data/lib/heathrow/sources/messenger_fetch_marionette.py +138 -0
- data/lib/heathrow/sources/messenger_send.js +32 -0
- data/lib/heathrow/sources/messenger_send.py +100 -0
- data/lib/heathrow/sources/reddit.rb +461 -0
- data/lib/heathrow/sources/rss.rb +299 -0
- data/lib/heathrow/sources/slack.rb +375 -0
- data/lib/heathrow/sources/source_manager.rb +328 -0
- data/lib/heathrow/sources/telegram.rb +498 -0
- data/lib/heathrow/sources/webpage.rb +207 -0
- data/lib/heathrow/sources/weechat.rb +479 -0
- data/lib/heathrow/sources/whatsapp.rb +474 -0
- data/lib/heathrow/ui/application.rb +8098 -0
- data/lib/heathrow/ui/navigation.rb +8 -0
- data/lib/heathrow/ui/panes.rb +8 -0
- data/lib/heathrow/ui/source_wizard.rb +567 -0
- data/lib/heathrow/ui/threaded_view.rb +780 -0
- data/lib/heathrow/ui/views.rb +8 -0
- data/lib/heathrow/version.rb +3 -0
- data/lib/heathrow/wizards/discord_wizard.rb +193 -0
- data/lib/heathrow/wizards/slack_wizard.rb +140 -0
- data/lib/heathrow.rb +55 -0
- metadata +147 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: af875b0f7d9ee70cf0f340207d21b5d367e6a8c98464361560c8b7649b2b6195
|
|
4
|
+
data.tar.gz: 30cc45fe89911cfe787a32a1e36e6d5d7a41a8e407779a2fb73cb0428b1b2e75
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: b2d822a3c2a09d17e13b8f579f4a91fe896145c303be3c51620dc059888ca710c733c302d705eaa7ecd0c709b79bb4524c831bc7f66aff937f39779b3c886c8a
|
|
7
|
+
data.tar.gz: 7ff0e102006c3bb8c33e527612890832120b67f9e3300840d20419bb04e67c0b50afd9b1896e951025c2abd913fe119b959ddf19554b1eb3a6947209a07341f8
|
data/.gitignore
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Ruby
|
|
2
|
+
*.gem
|
|
3
|
+
*.rbc
|
|
4
|
+
/.config
|
|
5
|
+
/coverage/
|
|
6
|
+
/InstalledFiles
|
|
7
|
+
/pkg/
|
|
8
|
+
/spec/reports/
|
|
9
|
+
/spec/examples.txt
|
|
10
|
+
/test/tmp/
|
|
11
|
+
/test/version_tmp/
|
|
12
|
+
/tmp/
|
|
13
|
+
|
|
14
|
+
# Bundler
|
|
15
|
+
/.bundle/
|
|
16
|
+
/vendor/bundle
|
|
17
|
+
/vendor/
|
|
18
|
+
/lib/bundler/man/
|
|
19
|
+
.bundle/
|
|
20
|
+
|
|
21
|
+
# Chitt specific
|
|
22
|
+
~/.chitt/
|
|
23
|
+
*.db
|
|
24
|
+
*.db-shm
|
|
25
|
+
*.db-wal
|
|
26
|
+
/logs/
|
|
27
|
+
/attachments/
|
|
28
|
+
|
|
29
|
+
# Environment
|
|
30
|
+
.env
|
|
31
|
+
.env.local
|
|
32
|
+
|
|
33
|
+
# Editor directories
|
|
34
|
+
.vscode/
|
|
35
|
+
.idea/
|
|
36
|
+
*.swp
|
|
37
|
+
*.swo
|
|
38
|
+
*~
|
|
39
|
+
|
|
40
|
+
# OS
|
|
41
|
+
.DS_Store
|
|
42
|
+
Thumbs.db
|
|
43
|
+
|
|
44
|
+
# Debug logs
|
|
45
|
+
debug.log
|
|
46
|
+
*.log
|
|
47
|
+
*.bak
|
|
48
|
+
|
|
49
|
+
# Personal test/setup scripts (contain credentials)
|
|
50
|
+
test_*.rb
|
|
51
|
+
setup_*.rb
|
|
52
|
+
import_*.rb
|
|
53
|
+
update_*.rb
|
|
54
|
+
workspace_login.rb
|
|
55
|
+
CLAUDE.md
|
|
56
|
+
.claude/
|
|
57
|
+
node_modules/
|
|
58
|
+
package*.json
|
data/README.md
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# Heathrow
|
|
2
|
+
|
|
3
|
+
<img src="img/heathrow.svg" align="right" width="150">
|
|
4
|
+
|
|
5
|
+
**Where all your messages connect.**
|
|
6
|
+
|
|
7
|
+
 [](https://badge.fury.io/rb/heathrow)  [](docs/) 
|
|
8
|
+
|
|
9
|
+
A unified TUI for all your communication. Like Heathrow Airport, every message routes through one hub. Replace mutt, newsboat, weechat, and a dozen chat apps with one terminal interface.
|
|
10
|
+
|
|
11
|
+
## The Problem
|
|
12
|
+
|
|
13
|
+
"Dad, I sent you a message." Which app? Email? WhatsApp? Discord? Telegram? SMS? Nobody should have to chase 4-5 apps to find a message from their kid.
|
|
14
|
+
|
|
15
|
+
## The Solution
|
|
16
|
+
|
|
17
|
+
All sources in one place. Create your custom views. Full keyboard control.
|
|
18
|
+
|
|
19
|
+

|
|
20
|
+
|
|
21
|
+
## Background
|
|
22
|
+
|
|
23
|
+
In 2007 I had the idea of One Communication Hub to rule them all. Now, with Claude Code, it was efficient to realize that idea. It has now replaced [mutt](http://mutt.org/) that I've been using since 2003, my RSS readers ([newsboat](https://newsboat.org/index.html) of late) and many other apps. Heathrow now functions as my communication hub :)
|
|
24
|
+
|
|
25
|
+
## Features
|
|
26
|
+
|
|
27
|
+
**Sources** (two-way read + reply):
|
|
28
|
+
- Email via Maildir (works with offlineimap, mbsync, fetchmail, gmail_fetch)
|
|
29
|
+
- IRC/Slack via WeeChat relay
|
|
30
|
+
- Discord, Telegram, Instagram DMs, Messenger
|
|
31
|
+
- Reddit
|
|
32
|
+
|
|
33
|
+
**Sources** (read-only):
|
|
34
|
+
- RSS/Atom feeds
|
|
35
|
+
- Web page change monitoring
|
|
36
|
+
|
|
37
|
+
**Core:**
|
|
38
|
+
- Threaded, flat, and folder-grouped view modes
|
|
39
|
+
- 10+ custom filtered views with AND/OR logic
|
|
40
|
+
- Compose, reply, forward with address book aliases (mutt-style)
|
|
41
|
+
- Postpone/recall drafts
|
|
42
|
+
- Dynamic message loading with jump-to-date
|
|
43
|
+
- Hidden HTML link extraction (SharePoint, OneDrive, etc.)
|
|
44
|
+
- Full-text search via notmuch
|
|
45
|
+
- Per-source colors, per-view threading, tag/star highlighting
|
|
46
|
+
- RTFM file picker for attachments
|
|
47
|
+
- Configurable editor args, SMTP, OAuth2
|
|
48
|
+
- AI assistant integration (Claude Code)
|
|
49
|
+
- First-time onboarding wizard
|
|
50
|
+
|
|
51
|
+

|
|
52
|
+
|
|
53
|
+
## Installation
|
|
54
|
+
|
|
55
|
+
### Requirements
|
|
56
|
+
|
|
57
|
+
- Ruby >= 2.7
|
|
58
|
+
- [rcurses](https://github.com/isene/rcurses) gem
|
|
59
|
+
- sqlite3 gem
|
|
60
|
+
|
|
61
|
+
### From RubyGems
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
gem install heathrow
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### From Source
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
git clone https://github.com/isene/heathrow.git
|
|
71
|
+
cd heathrow
|
|
72
|
+
gem install rcurses sqlite3
|
|
73
|
+
./bin/heathrow
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Quick Start
|
|
77
|
+
|
|
78
|
+
1. Run `heathrow`
|
|
79
|
+
2. The onboarding wizard guides you through adding your first source
|
|
80
|
+
3. Or press `S` to manage sources, `a` to add one
|
|
81
|
+
|
|
82
|
+
## Key Bindings
|
|
83
|
+
|
|
84
|
+
Press `?` for full help. Here are the essentials:
|
|
85
|
+
|
|
86
|
+
### Navigation
|
|
87
|
+
| Key | Action |
|
|
88
|
+
|-----|--------|
|
|
89
|
+
| `j`/`k` or arrows | Move up/down |
|
|
90
|
+
| `Enter` | Open message |
|
|
91
|
+
| `PgDn`/`PgUp` | Page through messages |
|
|
92
|
+
| `Home`/`End` | First/last message |
|
|
93
|
+
| `J` | Jump to date (yyyy-mm-dd) |
|
|
94
|
+
| `n`/`p` | Next/previous unread |
|
|
95
|
+
|
|
96
|
+
### Views
|
|
97
|
+
| Key | Action |
|
|
98
|
+
|-----|--------|
|
|
99
|
+
| `A` | All messages |
|
|
100
|
+
| `N` | New (unread) |
|
|
101
|
+
| `S` | Sources management |
|
|
102
|
+
| `0-9`, `F1-F12` | Custom filtered views |
|
|
103
|
+
| `G` | Cycle view mode (flat/threaded/folders) |
|
|
104
|
+
| `B` | Browse all folders |
|
|
105
|
+
| `F` | Browse favorite folders |
|
|
106
|
+
|
|
107
|
+
### Message Actions
|
|
108
|
+
| Key | Action |
|
|
109
|
+
|-----|--------|
|
|
110
|
+
| `r` | Reply |
|
|
111
|
+
| `e` | Reply with editor |
|
|
112
|
+
| `g` | Reply all |
|
|
113
|
+
| `f` | Forward |
|
|
114
|
+
| `m` | Compose new |
|
|
115
|
+
| `E` | Edit message as new |
|
|
116
|
+
| `R` | Toggle read/unread |
|
|
117
|
+
| `M` | Mark all in view as read |
|
|
118
|
+
| `*` | Toggle star |
|
|
119
|
+
| `t`/`T` | Tag message / tag all |
|
|
120
|
+
| `d` | Mark for deletion |
|
|
121
|
+
| `<` | Purge deleted |
|
|
122
|
+
| `x` | Open in browser |
|
|
123
|
+
| `v` | View attachments |
|
|
124
|
+
| `Y` | Copy right pane to clipboard |
|
|
125
|
+
| `y` | Copy message ID to clipboard |
|
|
126
|
+
|
|
127
|
+
### Compose Prompt
|
|
128
|
+
| Key | Action |
|
|
129
|
+
|-----|--------|
|
|
130
|
+
| `Enter` | Send |
|
|
131
|
+
| `e` | Edit (re-open editor) |
|
|
132
|
+
| `a` | Attach files (via RTFM picker) |
|
|
133
|
+
| `p` | Postpone (save as draft) |
|
|
134
|
+
| `ESC` | Cancel |
|
|
135
|
+
|
|
136
|
+
### UI
|
|
137
|
+
| Key | Action |
|
|
138
|
+
|-----|--------|
|
|
139
|
+
| `w` | Cycle pane width |
|
|
140
|
+
| `Ctrl-b` | Cycle border style |
|
|
141
|
+
| `D` | Cycle date format |
|
|
142
|
+
| `o` | Cycle sort order |
|
|
143
|
+
| `P` | Preferences popup |
|
|
144
|
+
| `?` | Help (press again for extended) |
|
|
145
|
+
| `q` | Quit |
|
|
146
|
+
|
|
147
|
+
## Configuration
|
|
148
|
+
|
|
149
|
+
All settings live in `~/.heathrow/heathrowrc` (Ruby syntax):
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
# UI
|
|
153
|
+
set :color_theme, 'Mutt'
|
|
154
|
+
set :date_format, '%b %-d'
|
|
155
|
+
set :sort_order, 'latest'
|
|
156
|
+
set :pane_width, 3
|
|
157
|
+
set :border_style, 1
|
|
158
|
+
|
|
159
|
+
# SMTP / OAuth2
|
|
160
|
+
set :default_email, 'you@example.com'
|
|
161
|
+
set :smtp_command, '~/bin/gmail_smtp'
|
|
162
|
+
set :safe_dir, '~/.heathrow/mail'
|
|
163
|
+
set :oauth2_script, '~/bin/oauth2.py'
|
|
164
|
+
set :oauth2_domains, %w[gmail.com]
|
|
165
|
+
|
|
166
|
+
# Identities (auto-selected by folder)
|
|
167
|
+
identity 'default',
|
|
168
|
+
from: 'You <you@example.com>',
|
|
169
|
+
signature: '~/.signature',
|
|
170
|
+
smtp: '~/bin/gmail_smtp'
|
|
171
|
+
|
|
172
|
+
# Custom views
|
|
173
|
+
view '1', 'Personal', folder: 'Personal'
|
|
174
|
+
view '2', 'Work', folder: 'Work'
|
|
175
|
+
view '9', 'RSS', source_type: 'rss'
|
|
176
|
+
|
|
177
|
+
# Favorite folders
|
|
178
|
+
set :favorite_folders, %w[Personal Work Archive]
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Most settings are also available via the `P` preferences popup.
|
|
182
|
+
|
|
183
|
+
## Architecture
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
+-- Heathrow
|
|
187
|
+
+-- rcurses TUI (panes, input, rendering)
|
|
188
|
+
+-- SQLite database (messages, sources, views, settings)
|
|
189
|
+
+-- Source plugins (maildir, rss, weechat, discord, ...)
|
|
190
|
+
+-- Message organizer (threading, grouping, sorting)
|
|
191
|
+
+-- Background poller (per-source sync intervals)
|
|
192
|
+
+-- Composer (editor integration, address book, SMTP)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Each source is a self-contained plugin. Sources crash independently without affecting the core. Filters default to "show all" on failure.
|
|
196
|
+
|
|
197
|
+
## Credits
|
|
198
|
+
|
|
199
|
+
- Created by Geir Isene with [Claude Code](https://claude.ai/claude-code)
|
|
200
|
+
- Built on [rcurses](https://github.com/isene/rcurses) TUI library
|
|
201
|
+
- Inspired by [RTFM](https://github.com/isene/RTFM) file manager
|
|
202
|
+
|
|
203
|
+
## License
|
|
204
|
+
|
|
205
|
+
[Unlicense](https://unlicense.org/) - released into the public domain.
|
data/bin/heathrow
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Heathrow - Communication Hub In The Terminal
|
|
5
|
+
# A unified TUI for all your communication sources
|
|
6
|
+
# Author: Geir Isene with Claude Code
|
|
7
|
+
# License: Public Domain
|
|
8
|
+
|
|
9
|
+
CRASH_LOG = '/tmp/heathrow-crash.log'
|
|
10
|
+
|
|
11
|
+
require_relative '../lib/heathrow'
|
|
12
|
+
|
|
13
|
+
begin
|
|
14
|
+
heathrow = Heathrow::Application.new
|
|
15
|
+
heathrow.run
|
|
16
|
+
rescue Interrupt
|
|
17
|
+
# Restore terminal
|
|
18
|
+
system('stty sane 2>/dev/null')
|
|
19
|
+
print "\e[?25h\e[0m\e[2J\e[H"
|
|
20
|
+
$stdout.flush
|
|
21
|
+
exit 0
|
|
22
|
+
rescue => e
|
|
23
|
+
# Log full crash details
|
|
24
|
+
crash = "#{Time.now.strftime('%Y-%m-%d %H:%M:%S')} CRASH in #{e.backtrace&.first}\n"
|
|
25
|
+
crash << " #{e.class}: #{e.message}\n"
|
|
26
|
+
crash << " Backtrace:\n"
|
|
27
|
+
e.backtrace&.first(20)&.each { |l| crash << " #{l}\n" }
|
|
28
|
+
crash << "\n"
|
|
29
|
+
File.write(CRASH_LOG, crash, mode: 'a')
|
|
30
|
+
|
|
31
|
+
# Restore terminal fully (stty sane handles raw mode reset)
|
|
32
|
+
system('stty sane 2>/dev/null')
|
|
33
|
+
print "\e[?25h\e[0m\e[2J\e[H"
|
|
34
|
+
$stdout.flush
|
|
35
|
+
|
|
36
|
+
# Print to stderr (rsh captures this better than stdout)
|
|
37
|
+
loc = e.backtrace&.first&.sub(/^.*lib\/heathrow\//, '')
|
|
38
|
+
$stderr.puts "Heathrow crashed: #{e.class}: #{e.message}"
|
|
39
|
+
$stderr.puts " at #{loc}"
|
|
40
|
+
$stderr.puts " Full log: #{CRASH_LOG}"
|
|
41
|
+
exit 1
|
|
42
|
+
end
|
data/bin/heathrowd
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Heathrow Daemon - Background polling service
|
|
5
|
+
# This runs independently of any Heathrow UI instances
|
|
6
|
+
# Author: Geir Isene with Claude Code
|
|
7
|
+
# License: Public Domain
|
|
8
|
+
|
|
9
|
+
require 'fileutils'
|
|
10
|
+
require 'logger'
|
|
11
|
+
require 'optparse'
|
|
12
|
+
require_relative '../lib/heathrow'
|
|
13
|
+
|
|
14
|
+
class HeathrowDaemon
|
|
15
|
+
def initialize(options = {})
|
|
16
|
+
@options = {
|
|
17
|
+
daemonize: false,
|
|
18
|
+
pid_file: File.join(HEATHROW_HOME, 'heathrowd.pid'),
|
|
19
|
+
log_file: File.join(HEATHROW_LOGS, 'heathrowd.log'),
|
|
20
|
+
poll_interval: 30 # Default 30 seconds between polling cycles
|
|
21
|
+
}.merge(options)
|
|
22
|
+
|
|
23
|
+
setup_logging
|
|
24
|
+
setup_components
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def start
|
|
28
|
+
if running?
|
|
29
|
+
@logger.info "Heathrow daemon already running (PID: #{read_pid})"
|
|
30
|
+
puts "Heathrow daemon already running"
|
|
31
|
+
return false
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
if @options[:daemonize]
|
|
35
|
+
daemonize
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
write_pid
|
|
39
|
+
setup_signal_handlers
|
|
40
|
+
|
|
41
|
+
@logger.info "Starting Heathrow daemon (PID: #{Process.pid})"
|
|
42
|
+
puts "Starting Heathrow daemon..." unless @options[:daemonize]
|
|
43
|
+
|
|
44
|
+
run_daemon
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def stop
|
|
48
|
+
unless running?
|
|
49
|
+
puts "Heathrow daemon not running"
|
|
50
|
+
return false
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
pid = read_pid
|
|
54
|
+
begin
|
|
55
|
+
Process.kill('TERM', pid)
|
|
56
|
+
puts "Stopping Heathrow daemon (PID: #{pid})"
|
|
57
|
+
|
|
58
|
+
# Wait for process to stop
|
|
59
|
+
30.times do
|
|
60
|
+
sleep 0.1
|
|
61
|
+
break unless running?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
if running?
|
|
65
|
+
Process.kill('KILL', pid)
|
|
66
|
+
puts "Force killed Heathrow daemon"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
cleanup_pid
|
|
70
|
+
true
|
|
71
|
+
rescue Errno::ESRCH
|
|
72
|
+
# Process already stopped
|
|
73
|
+
cleanup_pid
|
|
74
|
+
puts "Heathrow daemon was not running"
|
|
75
|
+
false
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def status
|
|
80
|
+
if running?
|
|
81
|
+
pid = read_pid
|
|
82
|
+
puts "Heathrow daemon running (PID: #{pid})"
|
|
83
|
+
true
|
|
84
|
+
else
|
|
85
|
+
puts "Heathrow daemon not running"
|
|
86
|
+
false
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def setup_logging
|
|
93
|
+
@logger = Logger.new(@options[:log_file])
|
|
94
|
+
@logger.level = Logger::INFO
|
|
95
|
+
@logger.formatter = proc do |severity, datetime, progname, msg|
|
|
96
|
+
"[#{datetime.strftime('%Y-%m-%d %H:%M:%S')}] #{severity}: #{msg}\n"
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def setup_components
|
|
101
|
+
@db = Heathrow::Database.new
|
|
102
|
+
@plugin_manager = Heathrow::PluginManager.new(@db)
|
|
103
|
+
@running = false
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def daemonize
|
|
107
|
+
# Fork and detach from terminal
|
|
108
|
+
Process.fork && exit
|
|
109
|
+
Process.setsid
|
|
110
|
+
Process.fork && exit
|
|
111
|
+
|
|
112
|
+
# Redirect standard streams
|
|
113
|
+
STDIN.reopen('/dev/null')
|
|
114
|
+
STDOUT.reopen('/dev/null', 'w')
|
|
115
|
+
STDERR.reopen('/dev/null', 'w')
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def write_pid
|
|
119
|
+
File.write(@options[:pid_file], Process.pid.to_s)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def read_pid
|
|
123
|
+
return nil unless File.exist?(@options[:pid_file])
|
|
124
|
+
File.read(@options[:pid_file]).strip.to_i
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def cleanup_pid
|
|
128
|
+
File.delete(@options[:pid_file]) if File.exist?(@options[:pid_file])
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def running?
|
|
132
|
+
return false unless File.exist?(@options[:pid_file])
|
|
133
|
+
|
|
134
|
+
pid = read_pid
|
|
135
|
+
return false unless pid && pid > 0
|
|
136
|
+
|
|
137
|
+
begin
|
|
138
|
+
Process.kill(0, pid)
|
|
139
|
+
true
|
|
140
|
+
rescue Errno::ESRCH
|
|
141
|
+
# Process doesn't exist
|
|
142
|
+
cleanup_pid
|
|
143
|
+
false
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def setup_signal_handlers
|
|
148
|
+
%w[TERM INT].each do |signal|
|
|
149
|
+
Signal.trap(signal) do
|
|
150
|
+
@logger.info "Received #{signal} signal, shutting down..."
|
|
151
|
+
@running = false
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
Signal.trap('HUP') do
|
|
156
|
+
@logger.info "Received HUP signal, reloading configuration..."
|
|
157
|
+
# Could reload config here if needed
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def run_daemon
|
|
162
|
+
@running = true
|
|
163
|
+
@logger.info "Daemon started, polling every #{@options[:poll_interval]} seconds"
|
|
164
|
+
|
|
165
|
+
while @running
|
|
166
|
+
begin
|
|
167
|
+
poll_all_sources
|
|
168
|
+
sleep @options[:poll_interval]
|
|
169
|
+
rescue StandardError => e
|
|
170
|
+
@logger.error "Error in daemon loop: #{e.message}"
|
|
171
|
+
@logger.error e.backtrace.join("\n")
|
|
172
|
+
sleep 5 # Brief pause on error
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
@logger.info "Daemon shutting down"
|
|
177
|
+
cleanup_pid
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def poll_all_sources
|
|
181
|
+
sources = @db.get_sources(true) # Only enabled sources
|
|
182
|
+
@logger.debug "Checking #{sources.size} enabled sources"
|
|
183
|
+
|
|
184
|
+
sources.each do |source_data|
|
|
185
|
+
begin
|
|
186
|
+
poll_source(source_data)
|
|
187
|
+
rescue StandardError => e
|
|
188
|
+
@logger.error "Error polling source #{source_data['name']}: #{e.message}"
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def poll_source(source_data)
|
|
194
|
+
source = Heathrow::Source.new(
|
|
195
|
+
id: source_data['id'],
|
|
196
|
+
type: source_data['type'],
|
|
197
|
+
name: source_data['name'],
|
|
198
|
+
config: source_data['config'],
|
|
199
|
+
enabled: source_data['enabled'] == 1,
|
|
200
|
+
poll_interval: source_data['poll_interval'],
|
|
201
|
+
last_poll: source_data['last_poll']
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
return unless source.should_poll?
|
|
205
|
+
|
|
206
|
+
@logger.debug "Polling source: #{source.name}"
|
|
207
|
+
|
|
208
|
+
plugin = @plugin_manager.create_source(source.type, source)
|
|
209
|
+
return unless plugin
|
|
210
|
+
|
|
211
|
+
messages = plugin.fetch_messages
|
|
212
|
+
new_message_count = 0
|
|
213
|
+
|
|
214
|
+
messages.each do |msg_data|
|
|
215
|
+
message = Heathrow::Message.new(msg_data)
|
|
216
|
+
message.source_id = source.id
|
|
217
|
+
message.source_type = source.type
|
|
218
|
+
|
|
219
|
+
if @db.insert_message(message.to_h.values)
|
|
220
|
+
new_message_count += 1
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
@db.update_source_poll_time(source.id)
|
|
225
|
+
|
|
226
|
+
if new_message_count > 0
|
|
227
|
+
@logger.info "Fetched #{new_message_count} new messages from #{source.name}"
|
|
228
|
+
else
|
|
229
|
+
@logger.debug "No new messages from #{source.name}"
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Command line interface
|
|
235
|
+
if __FILE__ == $0
|
|
236
|
+
options = {}
|
|
237
|
+
|
|
238
|
+
OptionParser.new do |opts|
|
|
239
|
+
opts.banner = "Usage: heathrowd [options] {start|stop|restart|status}"
|
|
240
|
+
|
|
241
|
+
opts.on("-d", "--daemon", "Run as daemon") do
|
|
242
|
+
options[:daemonize] = true
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
opts.on("-i", "--interval SECONDS", Integer, "Polling interval (default: 30)") do |interval|
|
|
246
|
+
options[:poll_interval] = interval
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
opts.on("-p", "--pid FILE", "PID file location") do |pid_file|
|
|
250
|
+
options[:pid_file] = pid_file
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
opts.on("-l", "--log FILE", "Log file location") do |log_file|
|
|
254
|
+
options[:log_file] = log_file
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
opts.on("-h", "--help", "Show this help") do
|
|
258
|
+
puts opts
|
|
259
|
+
exit
|
|
260
|
+
end
|
|
261
|
+
end.parse!
|
|
262
|
+
|
|
263
|
+
command = ARGV[0]
|
|
264
|
+
unless %w[start stop restart status].include?(command)
|
|
265
|
+
puts "Usage: heathrowd [options] {start|stop|restart|status}"
|
|
266
|
+
exit 1
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
daemon = HeathrowDaemon.new(options)
|
|
270
|
+
|
|
271
|
+
case command
|
|
272
|
+
when 'start'
|
|
273
|
+
daemon.start
|
|
274
|
+
when 'stop'
|
|
275
|
+
daemon.stop
|
|
276
|
+
when 'restart'
|
|
277
|
+
daemon.stop
|
|
278
|
+
sleep 1
|
|
279
|
+
daemon.start
|
|
280
|
+
when 'status'
|
|
281
|
+
daemon.status
|
|
282
|
+
end
|
|
283
|
+
end
|