Skip to content

Commit 159ebec

Browse files
committed
[FIX] base: redirect libsass warnings to logging
libsass is currently hard-coded to emit deprecation warnings to stderr (sass/libsass#3079), though explicit warnings can be intercepted (I could not say whether they *are* by the Python bindings though). Since libsass 3.5 (released July 2017), extending compound selector is deprecated ([0][bc], [1][extendcompound]) triggering the following message: WARNING on line 6713, column 13 of stdin: Compound selectors may no longer be extended. Consider `@extend .form-control, :disabled` instead. See http://bit.ly/ExtendCompound for details. This is both basically impossible to see when running "full" logs, and extremely spammy when only showing "warning"-level logs and running tours, as each tour will cause at least one assets compilation, triggering the warning if using a non-ancient libsass. Because it's emitted directly to stderr, it's also difficult to filter out. This PR messes arounds with fd redirections (adding a helper for that, similar to `contextlib.redirect_stderr` but working at the fd level as that's what we need here) in order to capture the stderr output of libsass and re-emit it as a logging `warning` if non-empty, to make it both visible and filter-able. * Intercepts to a temporary file, as we don't really know how much *stuff* libsass may output, currently it's way below PIPE_BUF (to say nothing of the actual pipe buffers) but we've no way to know that will always be the case, so using a pipe would require a separate thread to ensure the pipe output gets read and we don't risk clogging up the pipe (and blocking libsass entirely). * Doesn't bother checking for content with `tell` beforehand, that seems unlikely to be a limiting factor: currently we always lseek + read, by first checking for content with `tell` we'd have a "best case" (libsass emitted no warnings) of lseek but a "worst case" (libsass emitted warnings) of lseek + lseek + read. * Emitting a logging event rather than a `Deprecation` warning, an actual deprecation warning might be useful if we were processing the actual source files and could provide a precise source location to `warn_explicit`, but here we're dealing with a concatenated pile of junk (the warning is signaled on line 6713, even the largest bootstrap scss file is barely above 1Kloc) so shoving it through logging seems sufficient. SAFETY ------ In multiprocessing context (workers / prefork mode), this should not be an issue because the local redirection of an fd should not affect other processes. In multithreading context, `libsass` does not seem to release the GIL so it *should not* be a concern, however there are small windows right before and after calling `libsass.compile` where the interpreter *could* decide to suspend the current thread. Python has no API for interpreter-level critical sections of any sort (as of 3.9), nor does it use a standard stream lock, so there is no clean way to completely protect against this race condition. We could try and set the switching interval to a ridiculous value (using `sys.setswitchinterval`), but that seems riskier than the unlikely possibility of reassigning a few logging messages to a libsass warning. [bc]: https://sass-lang.com/documentation/breaking-changes/extend-compound [extendcompound]: http://bit.ly/ExtendCompound
1 parent c006e97 commit 159ebec

File tree

2 files changed

+49
-16
lines changed

2 files changed

+49
-16
lines changed

odoo/addons/base/models/assetsbundle.py

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
# -*- coding: utf-8 -*-
2-
from collections import OrderedDict
3-
from datetime import datetime
4-
from subprocess import Popen, PIPE
52
import base64
63
import hashlib
74
import itertools
85
import json
96
import logging
107
import os
118
import re
9+
import tempfile
1210
import textwrap
1311
import uuid
12+
from collections import OrderedDict
13+
from datetime import datetime
14+
from subprocess import Popen, PIPE
1415

15-
import psycopg2
1616
try:
1717
import sass as libsass
1818
except ImportError:
@@ -23,7 +23,7 @@
2323
from odoo import SUPERUSER_ID
2424
from odoo.http import request
2525
from odoo.modules.module import get_resource_path
26-
from odoo.tools import func, misc, transpile_javascript, is_odoo_module, SourceMapGenerator
26+
from odoo.tools import func, misc, transpile_javascript, is_odoo_module, SourceMapGenerator, osutil
2727
from odoo.tools.misc import html_escape as escape
2828
from odoo.tools.pycompat import to_text
2929

@@ -1017,17 +1017,25 @@ def compile(self, source):
10171017
if libsass is None:
10181018
return super(ScssStylesheetAsset, self).compile(source)
10191019

1020-
try:
1021-
return libsass.compile(
1022-
string=source,
1023-
include_paths=[
1024-
self.bootstrap_path,
1025-
],
1026-
output_style=self.output_style,
1027-
precision=self.precision,
1028-
)
1029-
except libsass.CompileError as e:
1030-
raise CompileError(e.args[0])
1020+
with tempfile.TemporaryFile() as newstderr:
1021+
try:
1022+
with osutil.redirect_fd(2, redirect_to=newstderr.fileno()):
1023+
return libsass.compile(
1024+
string=source,
1025+
include_paths=[
1026+
self.bootstrap_path,
1027+
],
1028+
output_style=self.output_style,
1029+
precision=self.precision,
1030+
)
1031+
except libsass.CompileError as e:
1032+
raise CompileError(e.args[0])
1033+
finally:
1034+
newstderr.seek(0)
1035+
warnings = newstderr.read().strip()
1036+
if warnings:
1037+
_logger.getChild('libsass').warning("%s", warnings.decode())
1038+
10311039

10321040
def get_command(self):
10331041
try:

odoo/tools/osutil.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,31 @@ def close_srv(srv):
166166
except Exception:
167167
return False
168168

169+
170+
@contextmanager
171+
def redirect_fd(fd, *, redirect_to):
172+
""" Redirects anything written to `fd` to `redirect_to`. Restores `fd` on
173+
contextmanager exit.
174+
175+
Similar to :class:`python:contextlib.redirect_stdout`, but works at the fd
176+
level instead of the file-like object level.
177+
178+
.. warning::
179+
180+
When applied to global fds (e.g. standard streams), this obviously has
181+
global effects.
182+
183+
:param int fd:
184+
:param int redirect_to:
185+
"""
186+
oldfd = os.dup(fd)
187+
try:
188+
os.dup2(redirect_to, fd)
189+
yield
190+
finally:
191+
os.dup2(oldfd, fd)
192+
os.close(oldfd)
193+
169194
if __name__ == '__main__':
170195
from pprint import pprint as pp
171196
pp(listdir('../report', True))

0 commit comments

Comments
 (0)