Skip to content

Offer a means to send the tty to the Apple log system#196

Closed
akimd wants to merge 12 commits intomoby:masterfrom
akimd:asl
Closed

Offer a means to send the tty to the Apple log system#196
akimd wants to merge 12 commits intomoby:masterfrom
akimd:asl

Conversation

@akimd
Copy link
Contributor

@akimd akimd commented Mar 29, 2018

On Docker for Windows, all the logs are aggregated: GUI, LinuxKit, etc. Currently debugging on Docker for Mac is less easy, in particular because LinuxKit's log are sent to the consolefile rings of fixed sized buffers. As a result, it's hard to find the interleaving of actions between Docker for Mac and LinuxKit, the console files are fragmented and full of '\0', etc.

It is possible to have hyperkit redirect the tty's output to its stderr/stdout, and then its caller (Docker for Mac's driver) could redirect these streams to the logger. Unfortunately on recent macOS releases, asl is replaced by os_log which uses exclusively the path of the executable as source (changing argv[0] is useless). As a result, hyperkit's logs are "credited" to the driver.

The most elegant approach is to embrace asl/os_log.

Currently Docker for Mac still targets 10.11, and os_log appeared in macOS 10.12, so we cannot use directly os_log's API, we use it through the asl shim.

  • add a new device configuration, asl, that sends the tty's output to asl
  • in Go, add a new ConsoleLog enum to enum it.

Since some of the logs use escapes for the terminal, escapes appear in the logs. But that's another issue.

2018-03-30 10:09:04.784291 +0200	par défaut	75122	com.docker.hyperkit	\^[[34mINFO\^[[0m[0000] starting containerd                           \^[[34mmodule\^[[0m=containerd \^[[34mrevision\^[[0m=9b55aab90508bd389d7654c4baf173a981477d55 \^[[34mversion\^[[0m=v1.0.1
2018-03-30 10:09:04.785597 +0200	par défaut	75122	com.docker.hyperkit	\^[[34mINFO\^[[0m[0000] loading plugin "io.containerd.content.v1.content"...  \^[[34mmodule\^[[0m=containerd \^[[34mtype\^[[0m=io.containerd.content.v1
2018-03-30 10:09:04.786756 +0200	par défaut	75122	com.docker.hyperkit	\^[[34mINFO\^[[0m[0000] loading plugin "io.containerd.snapshotter.v1.btrfs"...  \^[[34mmodule\^[[0m=containerd \^[[34mtype\^[[0m=io.containerd.snapshotter.v1
2018-03-30 10:09:04.788161 +0200	par défaut	75122	com.docker.hyperkit	\^[[33mWARN\^[[0m[0000] failed to load plugin io.containerd.snapshotter.v1.btrfs  \^[[33merror\^[[0m="path /var/lib/containerd/io.containerd.snapshotter.v1.btrfs must be a btrfs filesystem to be used with the btrfs snapshotter" \^[[33mmodule\^[[0m=containerd

@akimd akimd force-pushed the asl branch 5 times, most recently from f7f7584 to 888ac48 Compare March 29, 2018 15:12
@rn
Copy link
Member

rn commented Mar 29, 2018

Couple of generic comments:

  • for C code please follow CODINGSTYLE.md
  • there is a bunch of conditional code for apple/darwin. HyperKit is specifically for Darwin so there is no need for that unless the source is shared with bhyve

akimd added 3 commits March 30, 2018 08:53
Signed-off-by: Akim Demaille <akim.demaille@docker.com>
In some earlier experiment this function was called several times.
Since it made the code slightly easier to understand, I left it.

Signed-off-by: Akim Demaille <akim.demaille@docker.com>
Hyperkit.check verifies that if `h.Console == ConsoleStdio`, then
either `isTerminal(os.Stdout)` or `h.StateDir != ""`.

In the next commit, the device configuration becomes more complex, and
simplifying this conditional by eliminating impossible situations
makes this next commit clearer.

Signed-off-by: Akim Demaille <akim.demaille@docker.com>
@akimd akimd force-pushed the asl branch 2 times, most recently from 7179ee9 to cba5521 Compare March 30, 2018 07:08
@akimd akimd changed the title [wip] Asl Offer a means to send the tty to the logs Mar 30, 2018
@akimd akimd force-pushed the asl branch 2 times, most recently from 50794bf to d72b731 Compare March 30, 2018 08:19
@akimd akimd changed the title Offer a means to send the tty to the logs Offer a means to send the tty to the Apple log system Mar 30, 2018
@akimd
Copy link
Contributor Author

akimd commented Mar 30, 2018

Hi @rn. Thanks for the tips!

I have reached a point where this is really nice to use from Docker for Mac.

src/lib/asl.c Outdated
buf = malloc(buf_capacity);
} else {
buf_capacity *= 2;
buf = realloc(buf, buf_capacity);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both malloc and realloc can fail so you need to check here. Take extra care with realloc not to leak the original buffer -- although given there isn't much error handling you can do without a lot of reworking perhaps an xhyve_abort would be a reasonable choice and then leaks are somewhat immaterial.

You could also simplify using the fact that realloc(NULL, N) is equivalent to malloc(N).

Copy link
Contributor Author

@akimd akimd Apr 3, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not check that allocation succeeded, because there are many places where this is not done, starting with that very file

sc = calloc(1, sizeof(struct uart_softc));

What would you recommend? perror + exit?


struct ttyfd tty;
struct log log;
bool asl; /* Output to Apple logger. */
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GH's rendering suggests a tabs vs spaces issue here

} else if (strcmp("asl", backend) == 0) {
sc->asl = true;
asl_init();
retval = 0;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More tab/space issues?

go/hyperkit.go Outdated
}

// openTty opens the tty files for reading, and returns it.
func (h *HyperKit) openTty() *os.File {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TTY is an abbreviation so either openTTY or opentty throughout would be preferable.

Signed-off-by: Akim Demaille <akim.demaille@docker.com>
@akimd
Copy link
Contributor Author

akimd commented Apr 3, 2018

I uploaded a version that should address your concerns. Thanks!

Copy link
Member

@rn rn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

c725002 should be split up into two commits, one which changes the hyperkit code and one which updates the go code.

src/lib/asl.c Outdated
/* Grow buf/buf_capacity. */
static void buf_grow(void)
{
buf_capacity = buf_capacity ? 2 * buf_capacity : 1024;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not initialise the buffer capacity to 1024 in asl_init() and then just grow from there? Also, we should probably add an upper bound unconditionally flush if it is reached. Or more generally, is there a reason you just don't allocate a fixed buffer of, say 4k and flush it when it's full (or on \n). Seem's much simpler.

Copy link
Contributor Author

@akimd akimd Apr 3, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hesitated, but the code was so simple to make it dynamic, that I chose the safest path.

src/lib/asl.c Outdated
static aslmsg log_msg = NULL;

static unsigned char *buf = NULL;
static size_t buf_size = 0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buf_size seems misnamed, as it is not the size of the buffer, but rather the index into the buffer where to write.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would depend on what you call size. Size is the size of the contents of the buffer (0 when it's empty). Capacity is the "available size".

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In pretty much all the code I know which handles buffers in C, size/sz etc refers to the size of a buffer (not the size of the contents). IMHO, something like buf_idx is much clearer

ttywrite(&sc->tty, value);
if (sc->log.ring)
ringwrite(&sc->log, value);
if (sc->asl)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

below it looks like the asl and log are exclusive, as a else if is used while here it seems as if they can be used together.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see that they are exclusive. What do you mean?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry, I misread the snippet of code from below. This is fine.

src/lib/asl.c Outdated
if (!buf) {
perror("buf_grow");
exit(1);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd have used xhyve_abort but this is ok too I guess.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There were several exit(1) already. I don't know which coding style to follow, there are too many.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it depends on whether the code was originally kernel, library or application code in the FreeBSD bhyve stack. Looks like xhyve_abort is only in "kernel" code so that was a bad suggestion, sorry. In src/lib abort and exit are basically even so lets stick with exit.

src/lib/asl.c Outdated
if (buf_size + 1 >= buf_capacity) {
buf_grow();
}
if (c == '\n') {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm slightly concerned about what happens if the console is e.g. running some sort of curses app or something where \n might conceivably be quite rare, it could end up building up quite a large buffer.

Perhaps add an upper bound on the buffer and flush then even if no \n is observed?

Also don't you need to handle \0 specially here? Without such handling the buffer passed to asl_log will be truncated at any NUL in the stream. Perhaps just treat \0 as equivalent to \n?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have no idea what can be sent to tty here, whatever you suggest, I'll buy! I'm fine with your \0 = \n approach, or \0 = \\0 to make it visible, etc.

If you are aware of a tty2text approach, I'll take it too.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think what can be sent to the console here is effectively arbitrary bytes, could be anything, from raw binary to utf-16 encoded text, there's probably no good universal answer. Most frequently it'll be ASCII or UTF-8 though plus some escape codes, I expect.

How about:

  • Put an upper limit of 4K on the buffer. If we reach this limit then flush unconditionally (could also make it a static 4K buffer if you like since the dynamism is no longer that important with a static limit in place).
  • Treat \n and \0 the same: do not inject them in the log stream, instead flush the current buffer.
  • For \ insert it as \\ (i.e. escape it, to distinguish from the result of the next rule)
  • If !isprint(c) then escape it as hex \xx
  • otherwise simply insert c as is.

This should preserve all of the useful text in the common case without injecting control characters or anything too weird into ASL (which I don't expect to handle anything especially crazy). It's not quite reversible since we've lost the distinction between \n and \0 but I think it'll do, including the \n would make things pretty unreadable in the common case.

The escaping makes the buffer size check a little more complicated, since it's no longer 1:1. I suggest just growing when you reach capacity - 3 rather than being too fancy.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we put an upper limit of 4k on the buffer, why not just statically allocate it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like the escaping of !isprint. In my pet project I did exactly that, and as a result, every single UTF-8 non-ascii character was broken. I think we should not try to be too smart here.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at http://www.manpagez.com/man/3/asl_log/ I'm half wondering if perhaps ASL is already setup to deal with such things correctly. There's a bunch of ASL_ENCODING_* which look to only be used with asl_add_output_file but there seems to be a general implication that the internal DB treats things as NUL terminated strings ("All keys and values are NUL-character terminated C language strings. UTF-8 encoding may be used for non-ASCII characters.") and it understands escaping in the presentation layer to some extent.

So, while I'm a bit wary that we're going to bring things crashing down around us lets try just flushing on a) 4K limit and b) on \n or \0 and hope for the best.

akimd added 3 commits April 3, 2018 17:03
On Docker for Windows, all the logs are aggregated: GUI, LinuxKit,
etc.  Currently debugging on Docker for Mac is less easy, in
particular because LinuxKit's log are sent to the consolefile rings of
fixed sized buffers.  As a result, it's hard to find the interleaving
of actions between Docker for Mac and LinuxKit, the console files are
fragmented and full of '\0', etc.

It is possible to have hyperkit redirect the tty's output to its
stderr/stdout, and then its caller (Docker for Mac's driver) could
redirect these streams to the logger.  Unfortunately on recent macOS
releases, asl is replaced by os_log which uses exclusively the path of
the executable as source (changing `argv[0]` is useless).  As a
result, hyperkit's logs are "credited" to the driver.

The most elegant approach is to embrace asl/os_log.

Currently Docker for Mac still targets 10.11, and os_log appeared in
macOS 10.12, so we cannot use directly os_log's API, we use it through
the asl shim.

Add a new device configuration, asl, that sends the tty's output to asl.

Signed-off-by: Akim Demaille <akim.demaille@docker.com>
Signed-off-by: Akim Demaille <akim.demaille@docker.com>
Suggested by Rolf Neugebauer.

Signed-off-by: Akim Demaille <akim.demaille@docker.com>
Suggested by Ian Campbell.

Signed-off-by: Akim Demaille <akim.demaille@docker.com>
Suggested by Rolf Neugebauer.

Signed-off-by: Akim Demaille <akim.demaille@docker.com>
src/lib/asl.c Outdated
asl_log(asl, log_msg, ASL_LEVEL_NOTICE, "%s", buf);
buf_idx = 0;
} else {
if (buf_idx + 2 >= sizeof buf) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use sizeof(buf). If it were me I would also prefer to write buf_idx + 1 > sizeof(buf) but each to his own.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also prefer strict comparisons. And as a matter of fact, I also prefer stick to < and <= only, and never > or >= (lesson taught from Paul Eggert, a mentor of mine). But many people object to this style, and I wish to go forward, not open new debates.

Here, what I mean is if (! (buf_idx + 2 < sizeof buf)), so >= expresses more naturally the constraint.

Copy link
Member

@rn rn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After the code looks ok, the commit need to be re-arranged. There should be:

  • One commit adding ASL support to hyperkit
  • One commit for whitespace clean up
  • One commit to fix the link
  • One commit for the changes to the go bindings

src/lib/asl.c Outdated
buf_idx = 0;
} else {
if (buf_idx + 2 >= sizeof(buf)) {
/* Running out of space, flush. */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does this flush the buffer? I would also expect this to reset the buf_idx. Actually the size check should be in the first conditional, something like:
if \n or \0 or if buffer is full, flush it otherwise add character to buffer.

Copy link
Contributor Author

@akimd akimd Apr 4, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, asl_put('\n') flushes. Where should I start to help us see it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but then you start writing over the end of the buffer...

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It took me a moment to grok that too. How about adding asl_flush() with the contents of the \n case and calling that from both?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ijc Ok, good idea. @rn sorry, I don't understand what you mean.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rn I think buf_idx + 2 >= sizeof(buf) ensures there is enough space for the \0 which would be added for flushing. I'm not quite sure why it is +2 rather than +1 though, with the use of >= it seems like it would flush a byte too soon (but, meh).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ijc just pointed out the "subtle" recursive call to asl_put(). This is just plain crazy! don't do that.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose intruding on the asl_* namespace also used by the OS provided functions is moderately confusing. Perhaps our wrappers should all be xhyve_asl_* to avoid that?

akimd added 2 commits April 5, 2018 08:09
Suggested by Ian Campbell.

Signed-off-by: Akim Demaille <akim.demaille@docker.com>
Suggested by Ian Campbell.

Signed-off-by: Akim Demaille <akim.demaille@docker.com>
static void log_flush(void)
{
buf[buf_idx] = 0;
asl_log(log_client, log_msg, ASL_LEVEL_NOTICE, "%s", buf);
Copy link
Member

@rn rn Apr 5, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I read the code which calls this function correctly in neither case where this function is called do you null terminate buf. So, how does asl_log() know how long buf is?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @rn. Again, I'm sorry, but I don't know what you mean. It seems to me that you missed line 32, or I misunderstand your sentence.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry, my bad. shouldn't review code without having coffee before

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😄

if ((c == '\n') || (c == 0)) {
log_flush();
} else {
if (buf_idx + 2 >= sizeof(buf)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this now seems to waste one byte.

But more in general maybe have something like:

if (buf_idx + 2 >= sizeof(buf)) {
    buf[buf_idx++] = c;
    c = 0;
}
if ((c == '\n') || (c == 0)) {
    buf[buf_idx] = 0;
    asl_log(log_client, log_msg, ASL_LEVEL_NOTICE, "%s", buf);
    buf_idx = 0;
} else {
    buf[buf_idx++] = c;
}

This would keep everything in one function, but it's a matter of taste, i guess

Copy link
Contributor Author

@akimd akimd Apr 5, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I guess. I dislike assignments such as c=0 (I try to stick to some SSA form), and side-effects in expressions such as buf[buf_idx++]. Besides, in some situations you are now adding a \n (and 0) in the buffer.

@akimd
Copy link
Contributor Author

akimd commented Apr 9, 2018

Superseded by #199.

@akimd akimd closed this Apr 9, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants