Skip to content

Commit 2a4d4e0

Browse files
committed
Merge branch 'more-fixes'
2 parents 00ec975 + 7b9fd07 commit 2a4d4e0

5 files changed

Lines changed: 109 additions & 92 deletions

File tree

frameos/agent/frameos_agent.service

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ RestartSec=5
1414
LimitNOFILE=65536
1515
PrivateTmp=yes
1616
ProtectSystem=full
17-
ReadWritePaths=/etc/systemd/system /etc/cron.d /boot /boot/firmware
17+
ReadWritePaths=/etc/systemd/system /etc/cron.d /boot
1818

1919
[Install]
20-
WantedBy=multi-user.target
20+
WantedBy=multi-user.target

frameos/src/apps/data/haSensor/app.nim

Lines changed: 22 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import json, strformat, options, strutils, times, httpclient
22
import frameos/apps
33
import frameos/types
44
import frameos/runtime_diagnostics
5+
import frameos/utils/http_client
56

67
const
78
RequestTimeoutMs = 10000
@@ -23,13 +24,6 @@ proc error*(self: App, message: string): JsonNode =
2324
self.logError(message)
2425
return %*{"error": message}
2526

26-
proc guardResponseProgress(startedAt: float): proc(total, progress, speed: BiggestInt) {.closure, gcsafe.} =
27-
result = proc(total, progress, speed: BiggestInt) {.closure, gcsafe.} =
28-
if total > MaxResponseBytes.BiggestInt or progress > MaxResponseBytes.BiggestInt:
29-
raise newException(IOError, &"Home Assistant response exceeded {MaxResponseBytes} bytes")
30-
if epochTime() > startedAt + MaxResponseSeconds:
31-
raise newException(IOError, &"Home Assistant response exceeded {MaxResponseSeconds} seconds")
32-
3327
proc get*(self: App, context: ExecutionContext): JsonNode =
3428
let diagnosticsEnabled = self.frameConfig.debug
3529
if diagnosticsEnabled:
@@ -54,40 +48,29 @@ proc get*(self: App, context: ExecutionContext): JsonNode =
5448
if self.json.isSome and self.lastFetchAt + MinimumFetchIntervalSeconds > epochTime():
5549
return copy(self.json.get())
5650

57-
var client = newHttpClient(timeout = RequestTimeoutMs)
58-
try:
59-
client.headers = newHttpHeaders([
51+
let headers = newHttpHeaders([
6052
("Authorization", "Bearer " & accessToken),
6153
("Accept", "application/json"),
6254
("Accept-Encoding", "identity"),
6355
("Connection", "close")
64-
])
65-
var slashlessUrl = haUrl
66-
slashlessUrl.removeSuffix("/")
67-
let url = &"{slashlessUrl}/api/states/{self.appConfig.entityId}"
68-
if self.appConfig.debug:
69-
self.log("Fetching Home Assistant status from " & url)
70-
71-
try:
72-
client.onProgressChanged = guardResponseProgress(epochTime())
73-
let response = client.request(url)
74-
if response.code != Http200:
75-
return self.error "Error fetching Home Assistant status: HTTP " & $response.status
76-
77-
if response.contentLength() > MaxResponseBytes:
78-
return self.error &"Error fetching Home Assistant status: response exceeded {MaxResponseBytes} bytes"
79-
if response.body.len > MaxResponseBytes:
80-
return self.error &"Error fetching Home Assistant status: response exceeded {MaxResponseBytes} bytes"
56+
])
57+
var slashlessUrl = haUrl
58+
slashlessUrl.removeSuffix("/")
59+
let url = &"{slashlessUrl}/api/states/{self.appConfig.entityId}"
60+
if self.appConfig.debug:
61+
self.log("Fetching Home Assistant status from " & url)
8162

82-
let responseJson = parseJson(response.body)
83-
self.json = some(copy(responseJson))
84-
self.lastFetchAt = epochTime()
85-
if self.appConfig.debug:
86-
self.log($responseJson)
87-
return responseJson
88-
89-
except CatchableError as e:
90-
return self.error "Error fetching Home Assistant status: " & $e.msg
91-
92-
finally:
93-
client.close()
63+
try:
64+
let responseBody = boundedGetContent(url,
65+
headers = headers,
66+
timeoutMs = RequestTimeoutMs,
67+
maxBytes = MaxResponseBytes,
68+
maxSeconds = MaxResponseSeconds)
69+
let responseJson = parseJson(responseBody)
70+
self.json = some(copy(responseJson))
71+
self.lastFetchAt = epochTime()
72+
if self.appConfig.debug:
73+
self.log($responseJson)
74+
return responseJson
75+
except CatchableError as e:
76+
return self.error "Error fetching Home Assistant status: " & $e.msg

frameos/src/apps/data/rstpSnapshot/app.nim

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import osproc
33
import pixie
44
import strutils
55
import times
6+
import strformat
67
import frameos/apps
78
import frameos/types
89
import frameos/utils/image
@@ -35,16 +36,45 @@ proc runFfmpeg(command: string): tuple[data: string, exitCode: int] =
3536

3637
let outputPath = getTempDir() / ("frameos-rtsp-" & $getCurrentProcessId() & "-" & $(epochTime() * 1000).int & ".bmp")
3738
let commandWithOutput = command & " " & quoteShell(outputPath)
38-
var p = startProcess(commandWithOutput, options = {poUsePath, poEvalCommand, poDaemon})
39+
var p = startProcess(commandWithOutput, options = {poUsePath, poEvalCommand})
3940
defer:
40-
p.close()
41+
if p != nil:
42+
if p.running():
43+
try:
44+
p.terminate()
45+
discard p.waitForExit(1500)
46+
if p.running():
47+
p.kill()
48+
discard p.waitForExit(500)
49+
except CatchableError:
50+
discard
51+
p.close()
4152
if fileExists(outputPath):
4253
try:
4354
removeFile(outputPath)
4455
except OSError:
4556
discard
4657

47-
result.exitCode = p.waitForExit(FfmpegTimeoutMs)
58+
let startedAt = epochTime()
59+
while p.running():
60+
if fileExists(outputPath) and getFileSize(outputPath) > MaxFfmpegOutputBytes:
61+
p.terminate()
62+
discard p.waitForExit(1500)
63+
if p.running():
64+
p.kill()
65+
discard p.waitForExit(500)
66+
raise newException(IOError, "ffmpeg output exceeded " & $MaxFfmpegOutputBytes & " bytes")
67+
if epochTime() > startedAt + (FfmpegTimeoutMs.float / 1000.0):
68+
p.terminate()
69+
discard p.waitForExit(1500)
70+
if p.running():
71+
p.kill()
72+
discard p.waitForExit(500)
73+
result.exitCode = -1
74+
return
75+
sleep(100)
76+
77+
result.exitCode = p.waitForExit()
4878
if result.exitCode != 0:
4979
return
5080

@@ -69,8 +99,12 @@ proc get*(self: App, context: ExecutionContext): Image =
6999
let (data, exitCode) = runFfmpeg(command)
70100

71101
if exitCode != 0:
72-
self.logError "ffmpeg exited with code " & $exitCode
73-
return renderError(self, context, "ffmpeg failed to run (exit code " & $exitCode & ")")
102+
let reason = if exitCode == -1: "timeout after " & $(FfmpegTimeoutMs div 1000) & "s" else: "exit code " & $exitCode
103+
self.logError "ffmpeg failed: " & reason
104+
return renderError(self, context, "ffmpeg failed to run (" & reason & ")")
105+
106+
if data.len > MaxFfmpegOutputBytes:
107+
raise newException(IOError, &"ffmpeg output exceeded {MaxFfmpegOutputBytes} bytes")
74108

75109
try:
76110
return decodeImageWithFallback(data)

frameos/src/apps/data/rstpSnapshot/tests/test_app.nim

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ type
1111
HookMode = enum
1212
hmSuccess
1313
hmExitFailure
14+
hmTimeout
1415
hmDecodeFailure
1516
hmOSError
1617

@@ -33,6 +34,8 @@ proc fakeFfmpegRunner(command: string): tuple[data: string, exitCode: int] =
3334
result = (img.encodeImage(BmpFormat), 0)
3435
of hmExitFailure:
3536
result = ("", 7)
37+
of hmTimeout:
38+
result = ("", -1)
3639
of hmDecodeFailure:
3740
result = ("not-an-image", 0)
3841
of hmOSError:
@@ -76,6 +79,20 @@ suite "data/rstpSnapshot app":
7679
check outputImage.width == 13
7780
check outputImage.height == 7
7881

82+
test "ffmpeg timeout returns context-sized error image":
83+
let previousHook = rtspSnapshotFfmpegRunHook
84+
defer:
85+
rtspSnapshotFfmpegRunHook = previousHook
86+
87+
hookMode = hmTimeout
88+
rtspSnapshotFfmpegRunHook = fakeFfmpegRunner
89+
90+
let app = makeApp(FrameScene(logger: newLogger(LogStore(items: @[]))), FrameConfig(width: 9, height: 6))
91+
let outputImage = app.get(ExecutionContext(hasImage: true, image: newImage(13, 7)))
92+
93+
check outputImage.width == 13
94+
check outputImage.height == 7
95+
7996
test "decode failure branch returns frame-sized error image":
8097
let previousHook = rtspSnapshotFfmpegRunHook
8198
defer:
Lines changed: 29 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import json, strformat, options, times, strutils, httpclient
22
import frameos/types
3+
import frameos/utils/http_client
34

45
const
56
RequestTimeoutMs = 10000
@@ -37,13 +38,6 @@ proc log*(self: App, message: string) =
3738
proc error*(self: App, message: string) =
3839
self.scene.logger.log(%*{"event": "legacy/haSensor:error", "error": message})
3940

40-
proc guardResponseProgress(startedAt: float): proc(total, progress, speed: BiggestInt) {.closure, gcsafe.} =
41-
result = proc(total, progress, speed: BiggestInt) {.closure, gcsafe.} =
42-
if total > MaxResponseBytes.BiggestInt or progress > MaxResponseBytes.BiggestInt:
43-
raise newException(IOError, &"Home Assistant response exceeded {MaxResponseBytes} bytes")
44-
if epochTime() > startedAt + MaxResponseSeconds:
45-
raise newException(IOError, &"Home Assistant response exceeded {MaxResponseSeconds} seconds")
46-
4741
proc run*(self: App, context: ExecutionContext) =
4842
let haUrl = self.frameConfig.settings{"homeAssistant"}{"url"}.getStr
4943
if haUrl == "":
@@ -55,48 +49,37 @@ proc run*(self: App, context: ExecutionContext) =
5549
self.error("Please provide a Home Assistant access token in the settings.")
5650
return
5751

58-
var client = newHttpClient(timeout = RequestTimeoutMs)
59-
try:
60-
client.headers = newHttpHeaders([
52+
let headers = newHttpHeaders([
6153
("Authorization", "Bearer " & accessToken),
6254
("Accept", "application/json"),
6355
("Accept-Encoding", "identity"),
6456
("Connection", "close")
65-
])
66-
var slashlessUrl = haUrl
67-
slashlessUrl.removeSuffix("/")
68-
let url = &"{slashlessUrl}/api/states/{self.appConfig.entityId}"
69-
if self.appConfig.debug:
70-
self.log("Fetching Home Assistant status from " & url)
57+
])
58+
var slashlessUrl = haUrl
59+
slashlessUrl.removeSuffix("/")
60+
let url = &"{slashlessUrl}/api/states/{self.appConfig.entityId}"
61+
if self.appConfig.debug:
62+
self.log("Fetching Home Assistant status from " & url)
7163

72-
if self.json.isNone or self.lastFetchAt == 0 or self.lastFetchAt +
73-
self.appConfig.cacheSeconds < epochTime():
74-
try:
75-
client.onProgressChanged = guardResponseProgress(epochTime())
76-
let response = client.request(url)
77-
if response.code != Http200:
78-
self.error "Error fetching Home Assistant status: HTTP " &
79-
$response.status
80-
return
81-
if response.contentLength() > MaxResponseBytes:
82-
self.error &"Error fetching Home Assistant status: response exceeded {MaxResponseBytes} bytes"
83-
return
84-
if response.body.len > MaxResponseBytes:
85-
self.error &"Error fetching Home Assistant status: response exceeded {MaxResponseBytes} bytes"
86-
return
87-
self.json = some(parseJson(response.body))
88-
self.lastFetchAt = epochTime()
89-
except CatchableError as e:
90-
self.error "Error fetching Home Assistant status: " & $e.msg
91-
return
64+
if self.json.isNone or self.lastFetchAt == 0 or self.lastFetchAt +
65+
self.appConfig.cacheSeconds < epochTime():
66+
try:
67+
let responseBody = boundedGetContent(url,
68+
headers = headers,
69+
timeoutMs = RequestTimeoutMs,
70+
maxBytes = MaxResponseBytes,
71+
maxSeconds = MaxResponseSeconds)
72+
self.json = some(parseJson(responseBody))
73+
self.lastFetchAt = epochTime()
74+
except CatchableError as e:
75+
self.error "Error fetching Home Assistant status: " & $e.msg
76+
return
9277

93-
if self.json.isSome:
94-
let stateKey = if self.appConfig.stateKey ==
95-
"": "state" else: self.appConfig.stateKey
96-
self.scene.state[stateKey] = self.json.get()
97-
if self.appConfig.debug:
98-
self.log($self.scene.state[stateKey])
99-
else:
100-
self.error "No JSON response from Home Assistant"
101-
finally:
102-
client.close()
78+
if self.json.isSome:
79+
let stateKey = if self.appConfig.stateKey ==
80+
"": "state" else: self.appConfig.stateKey
81+
self.scene.state[stateKey] = self.json.get()
82+
if self.appConfig.debug:
83+
self.log($self.scene.state[stateKey])
84+
else:
85+
self.error "No JSON response from Home Assistant"

0 commit comments

Comments
 (0)