Skip to content

Commit 1a26308

Browse files
authored
Implement pack command timeout (#26)
1 parent 7abca1d commit 1a26308

File tree

5 files changed

+175
-110
lines changed

5 files changed

+175
-110
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
44

55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## [Unreleased]
8+
9+
- Added `pack` command timeout and updated `beet` and `lectern`
10+
711
## [0.7.0] - 2021-04-23
812

913
### Added
Lines changed: 21 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
import asyncio
22
import io
3-
from contextlib import asynccontextmanager
43
from logging import Logger, getLogger
5-
from typing import AsyncIterator
64

7-
from beet import FormattedPipelineException
8-
from beet.toolchain.utils import format_exc
95
from discord import File, Message
106
from discord.ext.commands import Bot, Cog, Context, command
117

@@ -17,6 +13,7 @@ def __init__(self, bot: Bot, **options):
1713
self.bot: Bot = bot
1814
self.log: Logger = getLogger(self.qualified_name)
1915
self.project_config = options
16+
self.build_timeout = options.pop("timeout", 5)
2017

2118
@command(name="pack")
2219
async def cmd_pack(self, ctx: Context):
@@ -28,40 +25,28 @@ async def cmd_pack(self, ctx: Context):
2825
author = message.author.display_name
2926
message_content = message.content.split("\n", 1)[-1]
3027

31-
self.log.info("%s: Running build for %s.", message.id, author)
28+
self.log.info("%s - Running build for %s.", message.id, author)
3229

3330
loop = asyncio.get_running_loop()
3431

35-
async with self.error_handler(ctx):
36-
attachments = await loop.run_in_executor(
37-
None, generate_packs, author, self.project_config, message_content
38-
)
39-
40-
if attachments:
41-
files = [
42-
File(io.BytesIO(data), filename=filename)
43-
for filename, data in attachments.items()
44-
]
45-
await ctx.send(files=files)
46-
else:
47-
await message.add_reaction("🤔")
48-
49-
self.log.info("%s: Done.", message.id)
50-
51-
@asynccontextmanager
52-
async def error_handler(self, ctx: Context) -> AsyncIterator[None]:
53-
try:
54-
yield
55-
except FormattedPipelineException as exc:
56-
message = exc.message
57-
exception = exc.__cause__ if exc.format_cause else None
58-
except Exception as exc:
59-
message = "An unhandled exception occurred. This could be a bug."
60-
exception = exc
32+
build_output, attachments = await loop.run_in_executor(
33+
None,
34+
generate_packs,
35+
self.project_config,
36+
self.build_timeout,
37+
author,
38+
message_content,
39+
)
40+
41+
content = f"```{joined}```" if (joined := "\n\n".join(build_output)) else ""
42+
files = [
43+
File(io.BytesIO(data), filename=filename)
44+
for filename, data in attachments.items()
45+
]
46+
47+
if content or files:
48+
await ctx.send(content, files=files)
6149
else:
62-
return
63-
64-
if exception:
65-
message += f"\n\n{format_exc(exception)}"
50+
await message.add_reaction("🤔")
6651

67-
await ctx.send(f"```{message}```")
52+
self.log.info("%s - Done.", message.id)

commanderbot_ext/ext/pack/pack_generate.py

Lines changed: 69 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,98 @@
11
import io
22
import os
3-
from typing import Dict
3+
from multiprocessing import Process, Queue
4+
from queue import Empty
5+
from typing import Dict, List, Tuple
46
from zipfile import ZipFile
57

6-
from beet import Context, ProjectConfig, config_error_handler, run_beet
8+
from beet import (
9+
Context,
10+
FormattedPipelineException,
11+
ProjectConfig,
12+
config_error_handler,
13+
run_beet,
14+
)
715
from beet.core.utils import JsonDict
16+
from beet.toolchain.utils import format_exc
817
from lectern import Document
918

19+
BuildResult = Tuple[List[str], Dict[str, bytes]]
20+
1021

1122
def generate_packs(
23+
project_config: JsonDict,
24+
build_timeout: float,
25+
project_name: str,
26+
message_content: str,
27+
) -> BuildResult:
28+
q: "Queue[BuildResult]" = Queue()
29+
30+
p = Process(target=worker, args=(q, project_name, project_config, message_content))
31+
p.start()
32+
p.join(timeout=build_timeout)
33+
34+
try:
35+
return q.get_nowait()
36+
except Empty:
37+
p.kill()
38+
return [
39+
f"Timeout exceeded. The message took more than {build_timeout} seconds to process."
40+
], {}
41+
42+
43+
def worker(
44+
q: "Queue[BuildResult]",
1245
project_name: str,
1346
project_config: JsonDict,
1447
message_content: str,
15-
) -> Dict[str, bytes]:
48+
):
1649
project_directory = os.getcwd()
17-
packs: Dict[str, bytes] = {}
1850

19-
message_config = {
51+
base_config = {
2052
"name": project_name,
2153
"pipeline": [__name__],
2254
"meta": {
2355
"source": message_content,
56+
"build_output": [],
57+
"build_attachments": {},
2458
},
2559
}
2660

27-
with config_error_handler():
28-
config = (
29-
ProjectConfig(**project_config)
30-
.resolve(project_directory)
31-
.with_defaults(ProjectConfig(**message_config).resolve(project_directory))
32-
)
61+
try:
62+
with config_error_handler():
63+
config = (
64+
ProjectConfig(**project_config)
65+
.resolve(project_directory)
66+
.with_defaults(ProjectConfig(**base_config).resolve(project_directory))
67+
)
68+
69+
with run_beet(config) as ctx:
70+
build_output = ctx.meta["build_output"]
71+
attachments = ctx.meta["build_attachments"]
72+
73+
for pack in ctx.packs:
74+
if pack:
75+
fp = io.BytesIO()
3376

34-
with run_beet(config) as ctx:
35-
for pack in ctx.packs:
36-
if not pack:
37-
continue
77+
with ZipFile(fp, mode="w") as output:
78+
pack.dump(output)
79+
output.writestr("source.md", message_content)
3880

39-
fp = io.BytesIO()
81+
attachments[f"{pack.name}.zip"] = fp.getvalue()
4082

41-
with ZipFile(fp, mode="w") as output:
42-
pack.dump(output)
43-
output.writestr("source.md", message_content)
83+
q.put((build_output, attachments))
84+
return
85+
except FormattedPipelineException as exc:
86+
build_output = [exc.message]
87+
exception = exc.__cause__ if exc.format_cause else None
88+
except Exception as exc:
89+
build_output = ["An unhandled exception occurred. This could be a bug."]
90+
exception = exc
4491

45-
packs[f"{pack.name}.zip"] = fp.getvalue()
92+
if exception:
93+
build_output.append(format_exc(exception))
4694

47-
return packs
95+
q.put((build_output, {}))
4896

4997

5098
def beet_default(ctx: Context):

0 commit comments

Comments
 (0)