1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
|
#
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
#
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
import hashlib
import logging
import os
import time
from twisted.internet.utils import getProcessOutputAndValue
import deluge.component as component
from deluge.common import windows_check
from deluge.configmanager import ConfigManager
from deluge.core.rpcserver import export
from deluge.event import DelugeEvent
from deluge.plugins.pluginbase import CorePluginBase
log = logging.getLogger(__name__)
DEFAULT_CONFIG = {'commands': []}
EXECUTE_ID = 0
EXECUTE_EVENT = 1
EXECUTE_COMMAND = 2
EVENT_MAP = {
'complete': 'TorrentFinishedEvent',
'added': 'TorrentAddedEvent',
'removed': 'TorrentRemovedEvent',
}
class ExecuteCommandAddedEvent(DelugeEvent):
"""
Emitted when a new command is added.
"""
def __init__(self, command_id, event, command):
self._args = [command_id, event, command]
class ExecuteCommandRemovedEvent(DelugeEvent):
"""
Emitted when a command is removed.
"""
def __init__(self, command_id):
self._args = [command_id]
class Core(CorePluginBase):
def enable(self):
self.config = ConfigManager('execute.conf', DEFAULT_CONFIG)
event_manager = component.get('EventManager')
self.registered_events = {}
self.preremoved_cache = {}
# Go through the commands list and register event handlers
for command in self.config['commands']:
event = command[EXECUTE_EVENT]
if event in self.registered_events:
continue
def create_event_handler(event):
def event_handler(torrent_id, *arg):
self.execute_commands(torrent_id, event, *arg)
return event_handler
event_handler = create_event_handler(event)
event_manager.register_event_handler(EVENT_MAP[event], event_handler)
if event == 'removed':
event_manager.register_event_handler(
'PreTorrentRemovedEvent', self.on_preremoved
)
self.registered_events[event] = event_handler
log.debug('Execute core plugin enabled!')
def on_preremoved(self, torrent_id):
# Get and store the torrent info before it is removed
torrent = component.get('TorrentManager').torrents[torrent_id]
info = torrent.get_status(['name', 'download_location'])
self.preremoved_cache[torrent_id] = [
torrent_id,
info['name'],
info['download_location'],
]
def execute_commands(self, torrent_id, event, *arg):
if event == 'added' and arg[0]:
# No futher action as from_state (arg[0]) is True
return
elif event == 'removed':
torrent_id, torrent_name, download_location = self.preremoved_cache.pop(
torrent_id
)
else:
torrent = component.get('TorrentManager').torrents[torrent_id]
info = torrent.get_status(['name', 'download_location'])
# Grab the torrent name and download location
# getProcessOutputAndValue requires args to be str
torrent_name = info['name']
download_location = info['download_location']
log.debug('Running commands for %s', event)
def log_error(result, command):
(stdout, stderr, exit_code) = result
if exit_code:
log.warning('Command "%s" failed with exit code %d', command, exit_code)
if stdout:
log.warning('stdout: %s', stdout)
if stderr:
log.warning('stderr: %s', stderr)
# Go through and execute all the commands
for command in self.config['commands']:
if command[EXECUTE_EVENT] == event:
command = os.path.expandvars(command[EXECUTE_COMMAND])
command = os.path.expanduser(command)
cmd_args = [
torrent_id.encode('utf8'),
torrent_name.encode('utf8'),
download_location.encode('utf8'),
]
if windows_check():
# Escape ampersand on windows (see #2784)
cmd_args = [cmd_arg.replace(b'&', b'^^^&') for cmd_arg in cmd_args]
if os.path.isfile(command) and os.access(command, os.X_OK):
log.debug('Running %s with args: %s', command, cmd_args)
d = getProcessOutputAndValue(command, cmd_args, env=os.environ)
d.addCallback(log_error, command)
else:
log.error('Execute script not found or not executable')
def disable(self):
self.config.save()
event_manager = component.get('EventManager')
for event, handler in self.registered_events.items():
event_manager.deregister_event_handler(event, handler)
log.debug('Execute core plugin disabled!')
# Exported RPC methods #
@export
def add_command(self, event, command):
command_id = hashlib.sha1(str(time.time()).encode()).hexdigest()
self.config['commands'].append((command_id, event, command))
self.config.save()
component.get('EventManager').emit(
ExecuteCommandAddedEvent(command_id, event, command)
)
@export
def get_commands(self):
return self.config['commands']
@export
def remove_command(self, command_id):
for command in self.config['commands']:
if command[EXECUTE_ID] == command_id:
self.config['commands'].remove(command)
component.get('EventManager').emit(
ExecuteCommandRemovedEvent(command_id)
)
break
self.config.save()
@export
def save_command(self, command_id, event, cmd):
for i, command in enumerate(self.config['commands']):
if command[EXECUTE_ID] == command_id:
self.config['commands'][i] = (command_id, event, cmd)
break
self.config.save()
|