Skip to content

Improving Command display/sort#42

Closed
gnarendran wants to merge 1 commit intohtop-dev:masterfrom
gnarendran:improved-command-display
Closed

Improving Command display/sort#42
gnarendran wants to merge 1 commit intohtop-dev:masterfrom
gnarendran:improved-command-display

Conversation

@gnarendran
Copy link
Copy Markdown
Contributor

@gnarendran gnarendran commented Aug 30, 2020

Addresses #40; Rebased from hishamhm/htop#809; Originally discussed in hishamhm/htop#801

With inputs from @Explorer09, @BenBE, @marxin

  1. On Linux a process is able to modify its own command name (/proc/pid/comm) say using prctl, and its own command line (/proc/pid/cmdline) say by writing into its arguments vector. But /proc/[pid]/exe is set by the kernel and provides the definitive location of the executable image. While the Command column currently displays cmdline, for the above reason it is desirable to display the executable image path and command name, along with cmdline, merging them where possible.
  • In this pull request, two new optional columns are added:
    i) "Comm" - displays /proc/[pid]/comm if readable. It may not be readable for zombies.
    ii) "Exe" - displays 15 characters of the basename of /proc/[pid]/exe if readable. htop is able to read the /proc/[pid]/exe of ALL processes only when htop is either run with root privilleges (done with care) or given the capability CAP_SYS_PTRACE (sudo setcap 'cap_sys_ptrace=ep' /usr/bin/htop).

  • A new option is added: "Merge exe, comm and cmdline in Command", toggled by the key 'm'
    If this option is unset (default for backwards compatibility), Command column displays cmdline as usual. If this option is set, exe, comm and cmdline are merged in the Command column, which is to be interpreted as follows:
    i) If no token is colorized, it implies htop didn't have permission to read /proc/[pid]/exe of the process, and htop has fallen back to displaying only cmdline.

    ii) If a token (which may have embedded spaces) is colorized, upto 15 bytes of it is understood to be comm. This implies htop was able to read the process' /proc/[pid]/exe and /proc/[pid]/comm. During display, htop first tries to find/merge comm in exe's basename; If that fails and also if the new option 'Try to find comm in cmdline' is set (default), htop tries to find/merge comm in cmdline (this may mis-identify a string in cmdline in very rare cases, say if comm or cmdline had been unsuitably modified).
    a) If comm was not merged into either exe or cmdline, three fields are displayed (with "│" as the field separator), the first being exe, the second comm, and the last cmdline.
    b) If comm was merged into exe or cmdline: If exe could not be merged with cmdline, two fields are displayed, the first being exe and the last cmdline;
    c) Otherwise, exe and cmdline are merged into a single field.

  1. While htop currently sorts/filters the Command based always on cmdline, irrespective of whether it is full path or basename that is displayed, it is desirable to sort/filter based on what is displayed. After this change, Command is sorted/filtered based on what is displayed, whether it is exe or cmdline, full path or basename.

  2. Implementation notes:

  • Command Name (/proc/[pid]/comm) is displayed in a separate color (PROCESS_COMM)
  • LinuxProcess_getCommandStr to return the displayed string (rather than cmdline always)
  • When reading /proc/[pid]/cmdline, cmdline is concatenated with '\n' (instead of space) as delimiter; This helps when looking for comm in basenames of cmdline tokens with embedded spaces. Later during display in RichString_writeFrom, '\n' is translated into space. - LinuxProcess_writeCommand displays exe, comm, and cmdline as described above, retaining the existing options like basename display/highlighting

Copy link
Copy Markdown
Member

@cgzones cgzones left a comment

Choose a reason for hiding this comment

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

I like the general idea.

Would it make sense to try to merge exe and cmdline, e.g for firefox I am getting:

/usr/lib/firefox/[green]firefox[green-end]|[purple]Web Content[purple_end]|/usr/lib/firefox/firefox -contentproc ...

@gnarendran
Copy link
Copy Markdown
Contributor Author

gnarendran commented Aug 31, 2020

I like the general idea.

Would it make sense to try to merge exe and cmdline, e.g for firefox I am getting:

/usr/lib/firefox/[green]firefox[green-end]|[purple]Web Content[purple_end]|/usr/lib/firefox/firefox -contentproc ...

In the above example firefox has modified its comm to 'Web Content'. Keeping the format 'exe|comm|cmdline' makes the merging of exe and cmdline impossible. On the other hand if one had coded a format 'exe|cmdline|comm' and cmdline is long (where merged with exe or modified and not merged), a modified (not merged) comm gets pushed to far right and maybe even out of the screen. As we desire exe to be the first column always, the current solution is a judicious compromise.

UPDATE: A new setting (on by default) "Try to strip exe from cmdline, in merged Command" addresses this suggestion, by stripping the redundant exe from cmdline and displaying:
/usr/lib/firefox/[green]firefox[green-end]|[purple]Web Content[purple_end]| -contentproc ...

@gnarendran gnarendran closed this Aug 31, 2020
@gnarendran gnarendran reopened this Aug 31, 2020
@marxin
Copy link
Copy Markdown

marxin commented Aug 31, 2020

I would like to thank @gnarendran for his patch. I've been using the patch for more than a year and I really like it.
There's a screenshot from what is displayed:
Screenshot from 2020-08-31 20-12-06

@gnarendran
Copy link
Copy Markdown
Contributor Author

gnarendran commented Sep 4, 2020 via email

@BenBE
Copy link
Copy Markdown
Member

BenBE commented Sep 7, 2020

Would it be possible to have exe, comm and cmdline be tree columns (with the exe column maybe just a basename) accompanied by an option for the cmdline column to (optionally) use the merging?

@gnarendran
Copy link
Copy Markdown
Contributor Author

gnarendran commented Sep 8, 2020

@BenBE Firstly, regarding displaying only exe's basename, I want to point out the main motivation behind this pull request is to know the process's executable path (from the standpoint of security), as any process is able to arbitrarily modify (and obfuscate) its own comm and cmdline. But exe can't be obfuscated. So showing exe's full path upon demand (like when 'p' key is toggled), is a necessity.

Regarding the columns:

  1. I think you don't mean presenting multiple tree columns simultaneously, as that will give redundant process hierarchy information, will occupy too much screen space, and will be confusing to look at etc..

  2. You might not even be meaning to have one tree column that has only one of exe, comm or cmdline, and display the other two as separate (configurable) optional normal columns, for the following reasons:

Given that both exe and cmdline are of variable length (pathname any where from 1 to 4095, even just basename from 1 to 255), when set as separate columns, one column will push the other one out of the screen. In fact for pleasing view there can be only one variable length column and it has to be the last (like the Command column of current htop). This is one reason behind incorporating all of exe, comm, cmdline in the last column. The other reason is that anyway in the case of > 90% of the processes, the three get merged, with comm shown in separate color, so no information is lost and screen space is optimally used - for example please see the screenshot at the end - there htop had the CAP_SYS_PTRACE capability, and was run with 'Show program path' ('p' key) disabled.

  1. You might be meaning to have only one tree column, and let the user configure (say by even hotkeys) what is displayed there - one of a) exe, b) comm, c) cmdline or d) a merged combination (say like this pull request). I agree this is a matter of taste, and others might choose to implement it this way. The reason I personally don't like giving these extra options is that a), b) and c) give insufficient information about whether exe was readable (say if htop didn't have permissions), whether comm/cmdline was modified etc., that d) anyway gives.

Screenshot from 2020-09-08 09-54-06

@BenBE
Copy link
Copy Markdown
Member

BenBE commented Sep 8, 2020

I figured as much regarding the limit of the tree view. My intention with that suggestion was to have e.g. an abbreviated exe column (with e.g. first 20 chars of basename) followed by the merged exe/comm/cmdline column. This would allow to easily sort by executable regardless of the actual command line. Apart from this I like the merged view as is; was more like some additional thoughts to provide exe and comm as additional columns (abbreviated) so these are easier to spot on the screen.

@gnarendran
Copy link
Copy Markdown
Contributor Author

gnarendran commented Sep 8, 2020

I figured as much regarding the limit of the tree view. My intention with that suggestion was to have e.g. an abbreviated exe column (with e.g. first 20 chars of basename) followed by the merged exe/comm/cmdline column. This would allow to easily sort by executable regardless of the actual command line. Apart from this I like the merged view as is; was more like some additional thoughts to provide exe and comm as additional columns (abbreviated) so these are easier to spot on the screen.

Thanks for the suggestions. Actually this pull request sorts by the first field displayed in the Command column - as exe is the first to be displayed, sort by exe is what this pull request does. If 'Show program path' (toggled by 'p' key) is set it displays/sorts by full path of exe, and if not it displays/sorts by the basename of exe.

Update:
On second reading I guess you wish to sort by comm OR exe's basename independent of what is displayed in the Command column - that this pull request does not do. For that one would indeed have to add separate columns for comm and abbreviated exe's - a separate project perhaps.

Also relevant:
comm should be already easy to spot as it is colorized (purple in the above screenshot). Similarly, 'Highlight program "basename"' is another (existing) useful option to make the exe's basename bold and stand out. So if exe's basename is the same as comm, it will be bold purple. Otherwise exe's basename will be bold Green etc.

@gnarendran
Copy link
Copy Markdown
Contributor Author

gnarendran commented Sep 10, 2020

@BenBE, @cgzones, @marxin, @Explorer09: I learnt that some users redirect htop output and script it, and they might be surprised by the new merged Command column. So for backwards compatilibity, have made the Command column display cmdline by default (as it is now), and merge only upon the new setting "Merge exe, comm and cmdline in Command" (toggled by the key 'm'). While at it, also added Exe (abbreviated basename) and Comm columns as suggested by @BenBE.

The pull request description has been updated with complete details.

@gnarendran gnarendran requested a review from cgzones September 10, 2020 09:36
@cgzones
Copy link
Copy Markdown
Member

cgzones commented Sep 10, 2020

Looks quite ok.
Could you please rebase onto the master branch instead of including merge commits and squash into 1 commit.
Also I noticed /home/username/projects/htop/htop and ./htop are not getting merged. Is that intended?

@gnarendran
Copy link
Copy Markdown
Contributor Author

gnarendran commented Sep 10, 2020

Looks quite ok.
Could you please rebase onto the master branch instead of including merge commits and squash into 1 commit.

@cgzones As I had done a sync (fetch upstream etc.) followed by my last commit, git log now shows commits from master in between my commits. Is there a recommended way to squash my commits alone?

Update: git rebase etc., seemed hard in this case, so this is what I did locally: a) Made a patch htop-dev-pull-42-patch.txt of my commits b) git reset --hard to revert all my commits c)git pull htop-dev d) apply the patch e) git commit f) git push --force.

BTW, thanks for the review.

Also I noticed /home/username/projects/htop/htop and ./htop are not getting merged. Is that intended?

Yes this is intentional. We only do a simple suffix/prefix match to merge. Handling ./htop etc., would require proper canonicalization (like handling also .///htop ../../projects/htop/./htop etc.,) - a bit out of scope.

@gnarendran gnarendran force-pushed the improved-command-display branch 3 times, most recently from c9fad7d to 5c85d0a Compare September 12, 2020 08:11
@gnarendran
Copy link
Copy Markdown
Contributor Author

gnarendran commented Sep 12, 2020

The push has updates to man page documenting the 'm' key and Exe, Comm columns, and coloring for thread/process basename in Exe column with kthread identification - just for completion.

Copy link
Copy Markdown
Member

@BenBE BenBE left a comment

Choose a reason for hiding this comment

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

Patch LGTM.

Wondering if one could simplify overall affairs if you cached comm, exe and cmdline in the process struct and for exe and cmdline also stored the basename offset. With these information merging cmdline/exe is basically 3 string compares: a) does exe match argv[0], b) does exe+exebase match argv[0]and c) doesexe+exebasematchargv[0]+argv0base. Once that's found you're left with matching commagainstexe+exebase`. Unless I'm missing something here.

The multitude of loops in the merging routines is kinda confusing to read.

if ((fd = open(filename, O_RDONLY)) != -1 &&
(amtRead = xread(fd, command, sizeof(command) - 1)) > 0) {
close(fd);
command[amtRead - 1] = 0;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is the last char read always one we want to discard? Like \n?

Copy link
Copy Markdown
Contributor Author

@gnarendran gnarendran Sep 12, 2020

Choose a reason for hiding this comment

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

  1. Regarding caching: LinuxProcessList_readCmdlineFile() indeed stores, in structure LinuxProcess: procComm, procExe, procExeBasenameOffset, procCmdlineBasenameOffset (argv0base in your comment); and in contained structure Process: cmdline (in a member called comm - this is the preexisting name, though confusing in the present context). So is your question why not move the procComm etc. from LinuxProcess, to the Process structure? That is because the new members are Platform (Linux) dependent. But if the question is why we re-read the members, it is because comm, cmdline can be changed anytime by the process, and even exe is changed by kernel when the process execve's (even while the pid remains the same.)
  2. The processing of cmdline is actually tricky. Normally processes store the arguments there separated by NUL (which are translated to '\n' when forming the cmdline string). But there are processes like chrome that store the entire argument vector separated by spaces (fun fact: in the upstream htop if you set hightlight basename, chrome's entire cmdline is highlighted!). Still other processes modify cmdline arbitrarily. In other words, the end of argv[0] is not readily/reliably obtainable from cmdline. So procCmdlineBasenameOffset in that argv[0] is also unreliable. I have left an example in the comments of matchCmdlinePrefixWithExeSuffix() "LinuxProcess.c:252" on how htop could get procCmdlineBasenameOffset wrong.
  3. Regarding merging in matchCmdlinePrefixWithExeSuffix(): Case a) is simple enough - it applies when cmdline[0] == '/' (absolute path, since exe always begins with '/'). b) is just a special case of c with procCmdlineBasenameOffset == 0. c) is a special case of d) that follows: Due to the unreliability of procCmdlineBasenameOffset, upon a match failure we have to search the cmdline backwards to see if there is another potential procCmdlineBasenameOffset - this is the outer iteration. d) This is the general case we wish to handle - cmdline prefix (not just the basename) is a suffix of exe. For example exe == '/A1/B2/C3/E4' and cmdine = 'C3/E4 ARG1 ARG2' and we want merge to succeed. In this case we match E4 (procCmdlineBasenameOffset) and reverse match upto C3, upto a valid relative path in exe (i.e. cmdline == '3/E4' should not match). This is the inner iteration, and it covers c) as well.

Is the last char read always one we want to discard? Like \n?

No this is just me attempting to be safe. It is true that when reading /proc/[pie]/comm we can expect that comm will be already NUL terminated.

Copy link
Copy Markdown
Contributor Author

@gnarendran gnarendran Sep 12, 2020

Choose a reason for hiding this comment

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

  1. LinuxProcess_writeCommand() where comm is matched first with exe's basename and then located in cmdline (if findCommInCmdline is set), is a bit complex mainly because of the various combination of cases to be considered: coloring the three components while showing all three of them, merging two of them or merging all of them; showing or not showing full path, highlighting basename etc.,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

With caching of those three values I mostly referred to "per refresh cycle". Didn't also take your case d) (cmd: C3/E4 args…) into account. My questions were based on a much more focussed view on primarily highlighting the basename and trying to match based on that.

Possibly related is the display of /snap/chromium/1298/usr/lib/chromium-browser/chrome being completely marked as "basename" without that patch, although for some other chrome processes skipping of the path AND proper highlighting of the basename works correctly. Have to test again with this patch, if the issue persists.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Possibly related is the display of /snap/chromium/1298/usr/lib/chromium-browser/chrome being completely marked as "basename" without that patch, although for some other chrome processes skipping of the path AND proper highlighting of the basename works correctly. Have to test again with this patch, if the issue persists.

But do note that without the setting 'Merge exe, comm and cmdline in Command' (toggled by 'm' key), even this pull-request falls back to old behavior of htop of displaying cmdline - then you would see the issue. With the setting (and with exe readable) of course this issue will not be seen as then basename is that of the exe, and reliably known.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Maybe have another PR for this after this is merged, that improves on extracting the correct basename in these cases. Based on some simple heuristic: If cmdline is argv[0] only assume separation by spaces instead. The exe is then assumed to be the first part before a space; from which basename is extracted.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

But identifying the basename on just space delimiter will mishandle cases where a directory name has spaces, like
cmdline == '/A1/B 2/E3 ARG1 ARG2'. Just saying there is no heuristics that can always correctly identify basename based upon cmdline alone, given the state of affairs that argv[0] is not always delimited by NUL in /proc/[pid]/cmdline and in fact can be modified arbitrarily. I do think the old basename identification heuristics in htop is reasonable (best effort possible) if it has only cmdline to work with. This is one of the reason for merging with the reliable exe (the other, main, reason being getting to know the path of the exe reliably).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

As it stands currently half of the processes started from chrome show chrome marked proerly as their basename and the others do not, which is kinda irritating.

Copy link
Copy Markdown
Contributor Author

@gnarendran gnarendran Sep 28, 2020

Choose a reason for hiding this comment

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

Are you referring to the the upstream htop that uses only cmdline for Command column (same as the unmerged Command that uses only cmdline in this pull request)? Yes, this is a known limitation with only using cmdline that I had already described. But the merged Command (toggle using 'm') in this pull request does fix this issue properly by using exe for basename - as I said this is one of the motivations of this pull request. Please see chrome in below screenshot of this pull request, with the setting "Merge exe, comm and cmdline in Command" ('m' toggle). However, please clarify if you mean some other issue with this pull request itself.

Or is the confusion due to the setting ("Merge exe, comm and cmdline in Command") being false by default, and you suggest it to be true by default?

Or may htop wasn't given capability sudo setcap 'cap_sys_ptrace=ep' htop to read the exe of other user's processes etc. and falling back to cmdline (unmerged Command) for those processes?

Screenshot from 2020-09-28 09-57-29

@cgzones cgzones added the RFE label Sep 12, 2020
@gnarendran
Copy link
Copy Markdown
Contributor Author

In the light of pull request #67, I have refactored LinuxProcess_writeCommand() into LinuxProcess_makeCommandStr() (which makes the merged command string deviod of newlines and ready to be displayed) and LinuxProcess_writeCommand() which colorizes suitably.

LinuxProcess_getCommandStr() returns the displayed string, the whole (all fields) of which may displayed/searched/sorted/filtered, whether or not 'Show merged Command' is set.

Also added color differentiation between comm of process and thread.

@gnarendran gnarendran force-pushed the improved-command-display branch from c60a3d6 to d9dd53a Compare September 20, 2020 16:28
@gnarendran
Copy link
Copy Markdown
Contributor Author

When compared with upstream htop-dev, the previous commit had a subtle change: When a process being tracked by htop became a zombie, that commit discarded the invalid exe/cmdline of the zombie, treating it just like a zombie that was already present when htop started (which is to only display its comm obtained from the stat file). But the upstream htop continues to
display the old cmdline of the process that became a zombie under its watch, as if its cmdline were still valid, and this appears to be intentional and more useful.

So in the latest commit, the handling of 'zombie under watch' is brought in line with upstream htop. Also made optimizations like not regenerating a Command string, unless its state - consisting of the relevant settings (showProgramPath, showMergedCommand, findCommInCmdline), and the processs' cmdline, comm and exe - has changed.

@lgtm-com
Copy link
Copy Markdown

lgtm-com bot commented Sep 20, 2020

This pull request introduces 2 alerts when merging d9dd53a into 5ea13e7 - view on LGTM.com

new alerts:

  • 2 for Potentially overflowing call to snprintf

Copy link
Copy Markdown
Member

@BenBE BenBE left a comment

Choose a reason for hiding this comment

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

2 potential buffer overflow issues, some code style issues.

process->basenameOffset = 0;
if (!process->comm)
process->basenameOffset = 0;
else
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please use braces for this if-statement as this is hard to read. Cf. discussion in #158.

Copy link
Copy Markdown
Contributor Author

@gnarendran gnarendran Sep 21, 2020

Choose a reason for hiding this comment

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

Ok, will change.

Comment on lines 719 to 884
for (int i = 0; i < amtRead; i++) {
if (command[i] == '\0' || command[i] == '\n') {
if (tokenEnd == 0) {
/* newline used as delimiter - when forming the mergedCommand, newline is
* converted to space by LinuxProcess_makeCommandStr */
if (command[i] == '\0')
command[i] = '\n';

if (command[i] == '\n') {
if (tokenEnd == 0)
tokenEnd = i;
}
command[i] = ' ';
} else {
/* htop considers the next character after the last / that is before
* basenameOffset, as the start of the basename in cmdline - see
* Process_writeCommand */
if (!tokenEnd && command[i] == '/')
tokenStart = i + 1;
lastChar = i;
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please use braces, even where they are optional. Readability of the source will be better.

Copy link
Copy Markdown
Contributor Author

@gnarendran gnarendran Sep 21, 2020

Choose a reason for hiding this comment

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

ok, will change.

Comment on lines +233 to +234
while (*++token == '\n')
;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Avoid the empty loop body. Better do { token++; } while('\n' == *token);.
Also the missing buffer length guarding against accidental reads behind the end of the buffer somewhat irritates me (Overall missing in this routine).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Standard idiom in string processing, but still will address to conform to htop coding style.

if (cmdline[0] == '/') {
matchLen = exeBaseLen + exeBaseOffset;
if (strncmp(cmdline, exe, matchLen) == 0 &&
((delim = cmdline[matchLen]) == 0 || delim == '\n' || delim == ' '))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Indentation; cf. #158.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The indentation here is 3 spaces just like other code in htop. If the reference is to the conditional's indent it is just standard vim indentation - seems no change is required here.

do {
/* match basename */
matchLen = exeBaseLen + cmdlineBaseOffset;
if (cmdlineBaseOffset < exeBaseOffset &&
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could <= work here too for the offset comparison?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

No, <= not needed, since cmdline[0] == '/' was already taken care off, cmdlineBaseOffset must be strictly < exeBaseOffset (as exe is guaranteed to start with '/').

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

k, was just unsure about the rationale.

*/
void LinuxProcess_makeCommandStr(Process* this) {
LinuxProcess *lp = (LinuxProcess *)this;
bool showMergedCommand = this->settings->showMergedCommand, showProgramPath = this->settings->showProgramPath,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Avoid declaring multiple variables in one line …

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Ok, will change.

return;
}

int commStart = 0, commEnd = 0, exeBasenameOffset = lp->procExeBasenameOffset,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Avoid multiple variables in one line.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Ok, will change.

Comment on lines +589 to +638
for (int i = 0; i < 32; i++)
if (indent & (1U << i))
maxIndent = i+1;
for (int i = 0; i < maxIndent - 1; i++) {
int written;
if (indent & (1 << i))
written = snprintf(buf, n, "%s ", CRT_treeStr[TREE_STR_VERT]);
else
written = snprintf(buf, n, " ");
buf += written;
n -= written;
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Indentation issue. Also use braces, even where optional.

Copy link
Copy Markdown
Contributor Author

@gnarendran gnarendran Sep 21, 2020

Choose a reason for hiding this comment

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

As mentioned in the comment, this code is from preexisting Process_writeField. However, will change.

Comment on lines +595 to +616
written = snprintf(buf, n, "%s ", CRT_treeStr[TREE_STR_VERT]);
else
written = snprintf(buf, n, " ");
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Use checked snprintf (currently xSnprintf in XAlloc.h; moved by #161 to String_snprintf in StringUtils.h) to avoid possible buffer overflow noted by LGTM checker.

Copy link
Copy Markdown
Contributor Author

@gnarendran gnarendran Sep 21, 2020

Choose a reason for hiding this comment

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

As mentioned in the comment, this code is from preexisting Process_writeField. However, will change if that is the coding standard.
UPDATE: You are right that the new LGTM doesn't like snprintf. So used stpcpy instead just so avoid the warning.
UPDATE2: The confusion arose because the Process_writeField been used as basis for the tree formatting above was before the snprintf (LGTM) fixes. So I was wondering how code already in the upstream was flagged. But I now see that the Process_writeField has also been updated with correct snprintf usage. Still I will stick with the rewrite with stpcpy as it is simpler.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm fine either way as long as the pitfalls are avoided.

@gnarendran
Copy link
Copy Markdown
Contributor Author

gnarendran commented Oct 19, 2020

Just noted a kinda strange display artifact, that although easy to explain, still looks kinda strange at first:

/usr/bin/python3.8 (deleted)│/usr/bin/python3 /usr/bin/smart-notifier

The /proc/[pid]/exe shows as '(deleted)' whenever the executable of the process currently running gets deleted/replaced. We could manually see this with readlink /proc/[pid]/exe. This exe is simply shown in merged Command - would consider this a feature (:-)), as it also provides useful information that the executable has been thus modified.

Also, I noticed that with the merging disabled some processes (some, but not all of Keybase, chromium to some extend) highlight the whole command+argument starting with the last / in the pathname. With the old behaviour they used to highlight the whole commandline string. Preferred target would be, that with merging disabled these processes have some sensible™ highlighting of just parts that make up a/the filename. E.g. look for first space that replaced by \0 gives existing filename might be a heuristic for this.

With non-merged Command (only cmdline is displayed), this pull request has the identical behavior as the old htop as per the code. I also verified it visually, by running old htop and this pull's htop (non-merged setting) in GNU screens and toggling between the screens. This is true whether or not show full path setting is on. Please do let me know if what you see differs. On the other hand, I agree that the old behavior itself, of highlighting the whole cmdline starting from last / in cases such as chrome is not satisfactory - but I do not know how it can be improved when only using cmdline - for example, assuming space as basename delimiter would break cases where basename legitimately has a space in it, and someone else might think this pull-req broke their case. So I suggest leaving the old behavior for non-merged Command as it is, and if any improvement could be thought of there, taking it up as a separate PR.

P.S.: Can we switch cyan and magenta in meaning? Having most of your process tree in magenta quite heavily changes the visual style of htop. ;-)

The thing is most of the nice colors were already taken - cyan for basename highlighting. Magenta was only the somewhat decent color left. And it had to be different from the non-merged Command to show that merging has taken place. The desire to keep the non-merged case completely backwards compatible leaves only this choice I think.

@BenBE
Copy link
Copy Markdown
Member

BenBE commented Oct 19, 2020

Just noted a kinda strange display artifact, that although easy to explain, still looks kinda strange at first:

/usr/bin/python3.8 (deleted)│/usr/bin/python3 /usr/bin/smart-notifier

The /proc/[pid]/exe shows as '(deleted)' whenever the executable of the process currently running gets deleted/replaced. We could manually see this with readlink /proc/[pid]/exe. This exe is simply shown in merged Command - would consider this a feature (:-)), as it also provides useful information that the executable has been thus modified.

Sure. Just wanted to note this, as it becomes quite visible at times.

Also, I noticed that with the merging disabled some processes (some, but not all of Keybase, chromium to some extend) highlight the whole command+argument starting with the last / in the pathname. With the old behaviour they used to highlight the whole commandline string. Preferred target would be, that with merging disabled these processes have some sensible™ highlighting of just parts that make up a/the filename. E.g. look for first space that replaced by \0 gives existing filename might be a heuristic for this.

With non-merged Command (only cmdline is displayed), this pull request has the identical behavior as the old htop as per the code. I also verified it visually, by running old htop and this pull's htop (non-merged setting) in GNU screens and toggling between the screens. This is true whether or not show full path setting is on. Please do let me know if what you see differs. On the other hand, I agree that the old behavior itself, of highlighting the whole cmdline starting from last / in cases such as chrome is not satisfactory - but I do not know how it can be improved when only using cmdline - for example, assuming space as basename delimiter would break cases where basename legitimately has a space in it, and someone else might think this pull-req broke their case. So I suggest leaving the old behavior for non-merged Command as it is, and if any improvement could be thought of there, taking it up as a separate PR.

The old view for Keybase (htop 3.0.2 from distro package):
image

The new behaviour for Keybase (htop from your branch):
image

P.S.: Can we switch cyan and magenta in meaning? Having most of your process tree in magenta quite heavily changes the visual style of htop. ;-)

The thing is most of the nice colors were already taken - cyan for basename highlighting. Magenta was only the somewhat decent color left. And it had to be different from the non-merged Command to show that merging has taken place. The desire to keep the non-merged case completely backwards compatible leaves only this choice I think.

What about using light green for the merged color? Isn't too far off from the basename color and kinda keeps with the general theme … We can move thread colors around at any time (dark blue normal threads/light blue with basename)

@gnarendran
Copy link
Copy Markdown
Contributor Author

gnarendran commented Oct 19, 2020

The old view for Keybase (htop 3.0.2 from distro package):
image

The new behaviour for Keybase (htop from your branch):
image

Thanks a lot for the examples. I installed keybase just to see what is happening: Here is the differing case, with redundant information removed:
/opt/keybase/Keybase ... /preload-main.bundle.js ... --shared-files=v8_snapshot_data:100

Old htop: highlight starts from first '/opt' ; New htop: highlight starts after the last / 'preload-main.bundle.js' (this is not visible in your image, but if we move to the right we see it).

In this case a bug in the Old Process_writeCommand() manifests:The old code intends to search for the last slash just like the new htop, but when it encounters a ':' in v8_snapshot_data:100 suddenly discards the search result, resulting in the highlight starting from the first /opt.
In the cases where ':' is not present in the cmdline, both Old and New htop give identical results. Clearly the presence of ':' in cmdline should not alter where the highlighting starts, and I think the new htop's behavior of at least always highlighting after the last slash is more consistent and as originally intended (though still with the limitations discussed before).

What about using light green for the merged color? Isn't too far off from the basename color and kinda keeps with the general theme … We can move thread colors around at any time (dark blue normal threads/light blue with basename)

I am surely open to changing the colors as long everyone will be fine with it. I can make a new commit if you could please suggest the choices (Ref: CRT.c) for PROCESS_COMM, PROCESS_THREAD_COMM, PROCESS_BASENAME, PROCESS_THREAD_BASENAME, for the color schemes COLORSCHEME_DEFAULT, COLORSCHEME_MONOCHROME, COLORSCHEME_BLACKONWHITE, COLORSCHEME_LIGHTTERMINAL, COLORSCHEME_MIDNIGHT, COLORSCHEME_BLACKNIGHT.

Edit: But please do try out the color choices before suggesting as not all combinations may be pleasing to the eye.

@marxin
Copy link
Copy Markdown

marxin commented Nov 5, 2020

May I please ping this again..

@fasterit fasterit added the needs-rebase Pull request needs to be rebased and conflicts to be resolved label Nov 5, 2020
@fasterit
Copy link
Copy Markdown
Member

fasterit commented Nov 5, 2020

May I please ping this again..

There's still unresolved bugs in this (highlighting), see comment #42 (comment) . Help fix em?

@gnarendran
Copy link
Copy Markdown
Contributor Author

May I please ping this again..

There's still unresolved bugs in this (highlighting), see comment #42 (comment) . Help fix em?

Actually that comment explains that no new "bug" has been introduced and the difference we see between the two images is in fact a fix/improvement over the pre-existing real bug (highlighting the entire cmdline when a ":" is present in the cmdline). However to reiterate the pre-existing limitation in old htop of highlighting from last '/' when working only with non-delimited cmdline is still present (in the non-merged view of this pull-req) and I don't think it will be appropriate to try to introduce new heuristics for that in this pull request, as that will break backwards compatibility in many cases. Best that it should be attempted, if at all, as a separate PR I think. Regards.

@fasterit
Copy link
Copy Markdown
Member

fasterit commented Nov 5, 2020

Actually that comment explains that no new "bug" has been introduced and the difference we see between the two images is in fact a fix/improvement over the pre-existing real bug (highlighting the entire cmdline when a ":" is present in the cmdline). However to reiterate the pre-existing limitation in old htop of highlighting from last '/' when working only with non-delimited cmdline is still present (in the non-merged view of this pull-req) and I don't think it will be appropriate to try to introduce new heuristics for that in this pull request, as that will break backwards compatibility in many cases. Best that it should be attempted, if at all, as a separate PR I think. Regards.

No, it's fine to fix this as it becomes more relevant with your merged command lines. This PR changes 600 lines, 5 more won't matter.

@gnarendran
Copy link
Copy Markdown
Contributor Author

Actually that comment explains that no new "bug" has been introduced and the difference we see between the two images is in fact a fix/improvement over the pre-existing real bug (highlighting the entire cmdline when a ":" is present in the cmdline). However to reiterate the pre-existing limitation in old htop of highlighting from last '/' when working only with non-delimited cmdline is still present (in the non-merged view of this pull-req) and I don't think it will be appropriate to try to introduce new heuristics for that in this pull request, as that will break backwards compatibility in many cases. Best that it should be attempted, if at all, as a separate PR I think. Regards.

No, it's fine to fix this as it becomes more relevant with your merged command lines. This PR changes 600 lines, 5 more won't matter.

The point is it is impossible to come up with good heuristics for non-merged cmdline that don't have delimiters (like in the case of chrome and keybase) and that is the reason for the old htop's behavior of highlighting from last slash. It was not fixed all these years not due to oversight, but it is a limitation that is practically impossible to overcome satisfactorily (because practically any character other than / can form a valid basename).

Having said that, let us not lose sight of the fact that in this pull request the user can simply use the merged Command ('m' toggle) to get perfect highlighting. It is only in the case of non-merged cmdline (which is provided for backwards compatibiity) this pull request highlights from the last slash like the old htop.

@fasterit
Copy link
Copy Markdown
Member

fasterit commented Nov 5, 2020

The point is it is impossible to come up with good heuristics for non-merged cmdline that don't have delimiters (like in the case of chrome and keybase) and that is the reason for the old htop's behavior of highlighting from last slash. It was not fixed all these years not due to oversight, but it is a limitation that is practically impossible to overcome satisfactorily (because practically any character other than / can form a valid basename).

It is fine as long as you do not merge, not optimal, but consistent. But now look at the screenshots above again, some Keybase -- whatever are highlighted now and some are not. That looks broken.

@gnarendran
Copy link
Copy Markdown
Contributor Author

gnarendran commented Nov 5, 2020

The point is it is impossible to come up with good heuristics for non-merged cmdline that don't have delimiters (like in the case of chrome and keybase) and that is the reason for the old htop's behavior of highlighting from last slash. It was not fixed all these years not due to oversight, but it is a limitation that is practically impossible to overcome satisfactorily (because practically any character other than / can form a valid basename).

It is fine as long as you do not merge, not optimal, but consistent. But now look at the screenshots above again, some Keybase -- whatever are highlighted now and some are not. That looks broken.

That is not true, in fact all are highlighted from the last slash, consistently. Just that in the case you mentioned the last slash is not seen in the image (it is far to the right.) This is the way it is meant to be - in this case old htop highlighted the whole cmdline (which is incorrect) due to the bug (as I explained) that it suddenly aborts the last slash only whenever it finds a ':'

Anyway, here is a suggestion - If someone wishes to rewrite the heuristics in Process_writeCommand() of old htop to everyone's satisfaction, well and good. We can wait for that commit and then we can emulate that heuristics in this pull request. Just that I don't wish to rewrite the heuristics myself, as it can be quite controversial (even I won't be satisfied with it frankly!) My main motivation for this pull request is the display of exe/comm/cmdline from security standpoint, and non-merged cmdline is only an afterthought for backwards compatibility. Best regards.

@BenBE
Copy link
Copy Markdown
Member

BenBE commented Nov 5, 2020

Furthermore one quite easy heuristic would be checking for NUL-bytes in the buffer (all the problematic cases lack them) and delimit by space. If you're really worried you could even try to stat the path and move to the next delimiter (e.g. space) if not found. Due to the executable name you usually already have a few hints to what you're likely to end up highlighting:

$ ls -la exe; echo; for a in cmdline comm; do hexdump -C $a; echo; done
lrwxrwxrwx 1 user user 0 Nov  5 13:37 exe -> /opt/keybase/Keybase

00000000  2f 6f 70 74 2f 6b 65 79  62 61 73 65 2f 4b 65 79  |/opt/keybase/Key|
00000010  62 61 73 65 20 2d 2d 74  79 70 65 3d 72 65 6e 64  |base --type=rend|
00000020  65 72 65 72 20 2d 2d 66  69 65 6c 64 2d 74 72 69  |erer --field-tri|
00000030  61 6c 2d 68 61 6e 64 6c  65 3d 30 30 30 30 30 30  |al-handle=000000|
00000040  30 30 30 30 30 30 30 30  30 30 30 30 30 2c 30 30  |0000000000000,00|
00000050  30 30 30 30 30 30 30 30  30 30 30 30 30 30 30 30  |0000000000000000|
00000060  30 2c 30 30 30 30 30 30  20 2d 2d 65 6e 61 62 6c  |0,000000 --enabl|
00000070  65 2d 66 65 61 74 75 72  65 73 3d 57 65 62 43 6f  |e-features=WebCo|
00000080  6d 70 6f 6e 65 6e 74 73  56 30 45 6e 61 62 6c 65  |mponentsV0Enable|
00000090  64 20 2d 2d 64 69 73 61  62 6c 65 2d 66 65 61 74  |d --disable-feat|
000000a0  75 72 65 73 3d 43 6f 6f  6b 69 65 73 57 69 74 68  |ures=CookiesWith|
000000b0  6f 75 74 53 61 6d 65 53  69 74 65 4d 75 73 74 42  |outSameSiteMustB|
000000c0  65 53 65 63 75 72 65 2c  53 61 6d 65 53 69 74 65  |eSecure,SameSite|
000000d0  42 79 44 65 66 61 75 6c  74 43 6f 6f 6b 69 65 73  |ByDefaultCookies|
000000e0  2c 53 70 61 72 65 52 65  6e 64 65 72 65 72 46 6f  |,SpareRendererFo|
000000f0  72 53 69 74 65 50 65 72  50 72 6f 63 65 73 73 20  |rSitePerProcess |
00000100  2d 2d 6c 61 6e 67 3d 64  65 20 2d 2d 61 70 70 2d  |--lang=de --app-|
00000110  70 61 74 68 3d 2f 6f 70  74 2f 6b 65 79 62 61 73  |path=/opt/keybas|
00000120  65 2f 72 65 73 6f 75 72  63 65 73 2f 61 70 70 20  |e/resources/app |
00000130  2d 2d 6e 6f 64 65 2d 69  6e 74 65 67 72 61 74 69  |--node-integrati|
00000140  6f 6e 20 2d 2d 6e 6f 2d  73 61 6e 64 62 6f 78 20  |on --no-sandbox |
00000150  2d 2d 6e 6f 2d 7a 79 67  6f 74 65 20 2d 2d 70 72  |--no-zygote --pr|
00000160  65 6c 6f 61 64 3d 2f 6f  70 74 2f 6b 65 79 62 61  |eload=/opt/keyba|
00000170  73 65 2f 72 65 73 6f 75  72 63 65 73 2f 61 70 70  |se/resources/app|
00000180  2f 64 65 73 6b 74 6f 70  2f 64 69 73 74 2f 70 72  |/desktop/dist/pr|
00000190  65 6c 6f 61 64 2d 6d 61  69 6e 2e 62 75 6e 64 6c  |eload-main.bundl|
000001a0  65 2e 6a 73 20 2d 2d 65  6e 61 62 6c 65 2d 72 65  |e.js --enable-re|
000001b0  6d 6f 74 65 2d 6d 6f 64  75 6c 65 20 2d 2d 62 61  |mote-module --ba|
000001c0  63 6b 67 72 6f 75 6e 64  2d 63 6f 6c 6f 72 3d 23  |ckground-color=#|
000001d0  66 66 66 66 66 66 20 2d  2d 65 6e 61 62 6c 65 2d  |ffffff --enable-|
000001e0  73 70 65 6c 6c 63 68 65  63 6b 20 2d 2d 65 6e 61  |spellcheck --ena|
000001f0  62 6c 65 2d 77 65 62 73  71 6c 20 2d 2d 64 69 73  |ble-websql --dis|
00000200  61 62 6c 65 2d 65 6c 65  63 74 72 6f 6e 2d 73 69  |able-electron-si|
00000210  74 65 2d 69 6e 73 74 61  6e 63 65 2d 6f 76 65 72  |te-instance-over|
00000220  72 69 64 65 73 20 2d 2d  6e 75 6d 2d 72 61 73 74  |rides --num-rast|
00000230  65 72 2d 74 68 72 65 61  64 73 3d 34 20 2d 2d 65  |er-threads=4 --e|
00000240  6e 61 62 6c 65 2d 6d 61  69 6e 2d 66 72 61 6d 65  |nable-main-frame|
00000250  2d 62 65 66 6f 72 65 2d  61 63 74 69 76 61 74 69  |-before-activati|
00000260  6f 6e 20 2d 2d 72 65 6e  64 65 72 65 72 2d 63 6c  |on --renderer-cl|
00000270  69 65 6e 74 2d 69 64 3d  35 20 2d 2d 6e 6f 2d 76  |ient-id=5 --no-v|
00000280  38 2d 75 6e 74 72 75 73  74 65 64 2d 63 6f 64 65  |8-untrusted-code|
00000290  2d 6d 69 74 69 67 61 74  69 6f 6e 73 20 2d 2d 73  |-mitigations --s|
000002a0  68 61 72 65 64 2d 66 69  6c 65 73 3d 76 38 5f 73  |hared-files=v8_s|
000002b0  6e 61 70 73 68 6f 74 5f  64 61 74 61 3a 31 30 30  |napshot_data:100|
000002c0  00                                                |.|
000002c1

00000000  4b 65 79 62 61 73 65 0a                           |Keybase.|
00000008

The only ever NUL byte that appears is the very last one read in cmdline, thus, after ignoring that one you simply assume you either got a program with no arguments (cmdline == &exe), or you split by any char ['\000'..'\040', ':'] (: unless followed by \).

Would be glad to receive real-world counter-examples for this heuristic, except the obvious one, when &exe contains spaces (which can be filtered by trying to stat that path and checking if what you found is a file).

NB: Example for running explorer.exe in wine (One that is currently broken in the release version):

/usr/lib/wine/wine64│explorer.exe│C:\windows\system32\explorer.exe /desktop
$ ls -la exe; echo; for a in cmdline comm; do hexdump -C $a; echo; done
lrwxrwxrwx 1 user user 0 Nov  5 13:37 exe -> /usr/lib/wine/wine64

00000000  43 3a 5c 77 69 6e 64 6f  77 73 5c 73 79 73 74 65  |C:\windows\syste|
00000010  6d 33 32 5c 65 78 70 6c  6f 72 65 72 2e 65 78 65  |m32\explorer.exe|
00000020  00 2f 64 65 73 6b 74 6f  70 00 00 00 00 00 00 00  |./desktop.......|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00     |...............|
0000003f

00000000  65 78 70 6c 6f 72 65 72  2e 65 78 65 0a           |explorer.exe.|
0000000d

@fasterit
Copy link
Copy Markdown
Member

fasterit commented Nov 5, 2020

Todo list for less confusing display:

  • better heuristics as per Improving Command display/sort #42 (comment) from @BenBE
  • toggle "Command" header to explain fields / active mode
  • make | (vertical bar) not highlight in rather random color
  • Column ids uppercase as all the others (COMM, EXE)
  • (suggestion) only make parts of the command line where information changes (i.e. EXE != COMM (first 15 chars) or a COMM has been set explicitly (the |COMM goes here| case)) magenta

@gnarendran
Copy link
Copy Markdown
Contributor Author

@BenBE @fasterit : Reply to the TODO list:

  1. Regarding the Heuristics:
    I am afraid the non-merged Cmdline limitation has not been understood fully:
  • Firstly there is no issue with cmdline that already has NUL delimiters between ARGV - in such cases the last slash identifies the basename correctly, both in old htop and this pull req. - this is case with 99% of the programs. The only issue is with 1% of the non-standard programs like chrome/keybase etc. that concatenate all arguments into cmdline.
  • There are cases when htop doesn't even have permission to read exe and in such cases as well we have only cmdline to work with. Furthermore even if htop is able to read exe, many times it does not match with cmdline, so then again exe can't be used in heuristics. BTW, the non-matched exe|cmdline are marked with red vertical bar - so the colors are not random - but are intended to convey information. Similarly there are cases where comm doesn't match cmdline.
  • In any case, if we do want to take advantage of exe, comm, just use the merged view 'm' toggle - there is no ambiguity whatsoever there.
  • The obvious case when the pathname has spaces (either components or basename) is more common than it is appreciated. It is much more common (due to users with Windows background working on LInux and porting of Windows programs etc/), than these non-standard non-NUL delimited cndline programs like chrome, keybase etc. Using space will break those other cases.
  • stat'ing for the longest valid filepath in cmdline will be expensive. Then again there are cases where cmdline is arbitrarily modified (simple example is "-/bin/bash" for login shell), and those will break.
  • This pull request is not about improving non-merged cmdline at all - it is all about including exe/comm in Command column.

For these reasons I don't think I should be touching the non-merged cmdline heuristics (apart from fixing the ':' bug in Process_writeCommand() that is already done).

  1. Documenting merged Command: Agree this should be done ideally, may be in man page - though the colorings can be intuitive when the user sees a few examples.
  2. Colors of vertical bar: As stated in 1. the colors are definitely not random - they have been carefully chosen to convey valuable information (RED for non-matched exe/cmdline etc.)
  3. Column ID upper case: Can be done, as it is it just followed the convention of Command column.
  4. Mark parts that differs (EXE != COMM) : this information is already present - COMM is always majenta and only if it maches with exe basename it is merged and basename becomes majenta etc. In fact all relevant information is conveyed by the colorings - mis-match or merge in COMM, EXE, CMDLINE etc.,

To sum up: Pending agreement on 1. (heuristics preservation), I can work on documenting (3.) and making the EXE and COMM uppercase (4.). Otherwise we can let this pull request be, and let someone else carry this forward or use it when needed, and no harm is done. I can understand that tastes differ when it comes to presentation and it is difficult to get uniform agreement. Thanks for all your inputs.

@BenBE
Copy link
Copy Markdown
Member

BenBE commented Nov 6, 2020

  • The only issue is with 1% of the non-standard programs like chrome/keybase etc. that concatenate all arguments into cmdline.

Note how my proposed heuristic addresses these 1% of cases and can work completely without having the EXE and COMM information available; just by working with the CMDLINE.

@fasterit
Copy link
Copy Markdown
Member

fasterit commented Nov 6, 2020

  • Documenting merged Command: Agree this should be done ideally, may be in man page - though the colorings can be intuitive when the user sees a few examples.

Man page is awesome, but the active mode should also be reflected in the header "Command" -> "Command (merged)"

  • Colors of vertical bar: As stated in 1. the colors are definitely not random - they have been carefully chosen to convey valuable information (RED for non-matched exe/cmdline etc.)

Think of this from the user perspective, magenta is close to red, it is an alerting color.
When people press "m" to switch on the merged view, currently things turn magenta where no information has changed. That is wrong. The merged view should only mark magenta what needs attention from the user, e.g. where the exe name and the cmdline don't match. I.e. on a system that is all in order very few lines should include magenta and that would be mostly Firefox forks that put their content into the comm.

So to summarize:

  1. unmerged view
  2. press "m"
  3. magenta only alerts to watch is worth attention
    (i.e. not to "I found and exe name")

@gnarendran
Copy link
Copy Markdown
Contributor Author

  • The only issue is with 1% of the non-standard programs like chrome/keybase etc. that concatenate all arguments into cmdline.

Note how my proposed heuristic addresses these 1% of cases and can work completely without having the EXE and COMM information available; just by working with the CMDLINE.

This has been discussed - any heurisitics that improves highlighting in the old cmdline for the 1% will break some other 1% (for example directories and basenames with spaces) and so is controversial. If someone still feels strongly about it, best to do the heuristics in the old htop (Process_writeCommand()), while this pull request waits, and then we can emulate that heuristics in this pull request - this was also suggested before, and modifying the old heuristics for highlighting non-merged cmdline is outside the scope of this pull request.

@gnarendran
Copy link
Copy Markdown
Contributor Author

gnarendran commented Nov 7, 2020

  • Documenting merged Command: Agree this should be done ideally, may be in man page - though the colorings can be intuitive when the user sees a few examples.

Man page is awesome, but the active mode should also be reflected in the header "Command" -> "Command (merged)"

  • Colors of vertical bar: As stated in 1. the colors are definitely not random - they have been carefully chosen to convey valuable information (RED for non-matched exe/cmdline etc.)

Think of this from the user perspective, magenta is close to red, it is an alerting color.
When people press "m" to switch on the merged view, currently things turn magenta where no information has changed. That is wrong. The merged view should only mark magenta what needs attention from the user, e.g. where the exe name and the cmdline don't match. I.e. on a system that is all in order very few lines should include magenta and that would be mostly Firefox forks that put their content into the comm.

So to summarize:

  1. unmerged view
  2. press "m"
  3. magenta only alerts to watch is worth attention
    (i.e. not to "I found and exe name")

I think two points are being missed here:

  • The 'm' toggle does merging only when possible - it will not be possible when exe of a process can't be read (due to lack of permission). In that case htop will default to old cmdline. In other words, Command column will have both merged and non-merged strings depending on whether exe was read for a given process
  • Given this, the change of color gives an important information (even when the text has not changed) - the information is that exe was read and matched with cmdine. This is important to be sure that cmdline/comm etc. have not been spoofed. The color differentiations have been carefully chosen to avoid any ambiguity and convey all information.

As I have already said, I am also not a fan of majenta, but out of 8 colors only this was available and decent. Regarding this please see my request for alternatives at end of the comment #42 (comment)

Agreed that changing the title to "Command (merged)" could be done - it might help the user if he sees a screen full of non-merged cmdline's (due to lack of permission).

@BenBE
Copy link
Copy Markdown
Member

BenBE commented Nov 7, 2020

Okay, based on your branch I implemented my heuristic (on Linux only for now).

Also note, I couldn't place Compat_faccessat into Compat.c where it would belong, due to the current branch not being up-to-date with master.

Thus take that patch as a PoC only for now.

Okay, here for my initial patch (configure.ac change is straight forward to use faccessat if present):

diff --git a/linux/LinuxProcessList.c b/linux/LinuxProcessList.c
index 4b6e4b4..d2ad999 100644
--- a/linux/LinuxProcessList.c
+++ b/linux/LinuxProcessList.c
@@ -838,6 +838,36 @@ static void setCommand(Process* process, const char* command, int len) {
    process->commLen = len;
 }
 
+static int Compat_faccessat(int dirfd, const char* pathname, int mode, int flags) {
+   int ret;
+
+#ifdef HAVE_FACCESSAT
+   // Implementation note: AT_SYMLINK_NOFOLLOW unsupported on FreeBSD, fallback to lstat in that case
+
+   errno = 0;
+
+   ret = faccessat(dirfd, pathname, mode, flags);
+   if (!ret || errno != EINVAL)
+      return ret;
+#endif
+
+   // Error out on unsupported configurations
+   if (dirfd != AT_FDCWD || mode != F_OK) {
+      errno = EINVAL;
+      return -1;
+   }
+
+   // Fallback to stat(2)/lstat(2) depending on flags
+   struct stat statinfo;
+   if(flags) {
+      ret = lstat(pathname, &statinfo);
+   } else {
+      ret = stat(pathname, &statinfo);
+   }
+
+   return ret;
+}
+
 static bool LinuxProcessList_readCmdlineFile(Process* process, const char* dirname, const char* name) {
    LinuxProcess *lp = (LinuxProcess *)process;
    char filename[MAX_NAME+1];
@@ -849,9 +879,7 @@ static bool LinuxProcessList_readCmdlineFile(Process* process, const char* dirna
    char command[4096+1]; // max cmdline length on Linux
    int amtRead = xread(fd, command, sizeof(command) - 1);
    close(fd);
-   int tokenEnd = 0;
-   int tokenStart = 0;
-   int lastChar = 0;
+
    if (amtRead == 0) {
       if (process->state == 'Z') {
          process->basenameOffset = 0;
@@ -862,11 +890,24 @@ static bool LinuxProcessList_readCmdlineFile(Process* process, const char* dirna
    } else if (amtRead < 0) {
       return false;
    }
+
+   int tokenEnd = 0;
+   int tokenStart = 0;
+   int lastChar = 0;
+   bool argSepNUL = false;
+   bool argSepSpace = false;
+
    for (int i = 0; i < amtRead; i++) {
       /* newline used as delimiter - when forming the mergedCommand, newline is
        * converted to space by LinuxProcess_makeCommandStr */
       if (command[i] == '\0') {
          command[i] = '\n';
+      } else {
+         /* Record some information for the argument parsing heuristic below. */
+         if (tokenEnd)
+            argSepNUL = true;
+         if (command[i] <= ' ')
+            argSepSpace = true;
       }
 
       if (command[i] == '\n') {
@@ -883,10 +924,83 @@ static bool LinuxProcessList_readCmdlineFile(Process* process, const char* dirna
          lastChar = i;
       }
    }
+
+   command[lastChar + 1] = '\0';
+
+   if (!argSepNUL && argSepSpace) {
+      /* Argument parsing heuristic.
+       *
+       * This heuristic is used for processes that rewrite their command line.
+       * Normally the command line is split by using NUL bytes between each argument.
+       * But some programs like chrome flatten this using spaces.
+       *
+       * This heuristic tries its best to undo this loss of information.
+       * To achieve this, we treat every character <= 32 as argument separators
+       * (i.e. all of ASCII control sequences and space).
+       * We then search for the basename of the cmdline in the first argument we found that way.
+       * As path names may contain we try to cross-validate if the path we got that way exists.
+       */
+
+      tokenStart = tokenEnd = 0;
+
+      // From initial scan we know there's at least one space.
+      // Check if that's part of a filename for an existing file.
+      if (Compat_faccessat(AT_FDCWD, command, F_OK, AT_SYMLINK_NOFOLLOW) != 0) {
+         // If we reach here the path does not exist.
+         // Thus begin searching for the part of it that actually is.
+
+         int tokenArg0Start = 0;
+
+         for (int i = 0; i <= lastChar; i++) {
+            /* Any ASCII control or space used as delimiter */
+            char tmpCommandChar = command[i];
+
+            if (command[i] <= ' ') {
+               if (!tokenEnd) {
+                  command[i] = '\0';
+
+                  bool found = Compat_faccessat(AT_FDCWD, command, F_OK, AT_SYMLINK_NOFOLLOW) == 0;
+
+                  // Restore if this wasn't it
+                  command[i] = found ? '\n' : tmpCommandChar;
+
+                  if (found)
+                     tokenEnd = i;
+                  if (!tokenArg0Start)
+                     tokenArg0Start = tokenStart;
+               } else {
+                  // Split on every further separator, regardless of path correctness
+                  command[i] = '\n';
+               }
+            } else if (!tokenEnd) {
+               if (command[i] == '/') {
+                  tokenStart = i + 1;
+               } else if (command[i] == '\\' &&(!tokenStart || command[tokenStart - 1] == '\\')) {
+                  tokenStart = i + 1;
+               }
+            }
+         }
+
+         if (!tokenEnd) {
+            tokenStart = tokenArg0Start;
+
+            // No token delimiter found, forcibly split
+            for (int i = 0; i <= lastChar; i++) {
+               if (command[i] <= ' ') {
+                  command[i] = '\n';
+                  if (!tokenEnd) {
+                     tokenEnd = i;
+                  }
+               }
+            }
+         }
+      }
+   }
+
    if (tokenEnd == 0) {
-      tokenEnd = amtRead;
+      tokenEnd = lastChar + 1;
    }
-   command[lastChar + 1] = '\0';
+
    lp->mergedCommand.maxLen = lastChar + 1;  /* accomodate cmdline */
    if (!process->comm || strcmp(command, process->comm)) {
       process->basenameOffset = tokenEnd;

@gnarendran
Copy link
Copy Markdown
Contributor Author

Okay, based on your branch I implemented my heuristic (on Linux only for now).

Also note, I couldn't place Compat_faccessat into Compat.c where it would belong, due to the current branch not being up-to-date with master.

Thus take that patch as a PoC only for now.

I understand that you intend to supplement (rather than replace) the old htop cmdline heuristics (highlight after last slash) with the new heuristics (using stat()).

  • But there could be breakages. For example, consider cmdline=="/A/B C/D" where EXE=="D", but "/A/B C/D" is not stat()able to htop (lack of read-permission in directory "/A/B C"). But if it so happens that "/A/B" exists (whether regular file or not) and stat()able, just "B" would be highlighted - whereas the old heuristics would correctly highlight "D". There could be other corner-cases.
  • From the cost PoV, to find the longest stat()able prefix of cmdline, for every space in cmdline there will be a stat() - in the case of chrome and its every thread, there would be 10 stat()'s each (into harddisk eventhough cached) or so. But I guess we are making that compromise for accuracy.
  • Also as you have noted, when stat() fails due to lack of permissions (say another user is testing web pages in different chrome version installed in his private directory), the new heuristics can't help - eventhough overall we may expect that accuracy improves to say 99.5% from 99%.

But as non-merged cmdline is not important to me personally, I have no objections to either the old 99% heuristics or the new 99.5% heuristics, but the breakages etc. might concern some other users.

A request: From this point onwards could you (or someone else) please carry this pull-req forward? It seems I have done all I could for the merged-Command and will not be able to come back to this anytime soon. Once the new owner's pull-req is merged I will close this pull-req.

@BenBE
Copy link
Copy Markdown
Member

BenBE commented Nov 8, 2020

A request: From this point onwards could you (or someone else) please carry this pull-req forward? It seems I have done all I could for the merged-Command and will not be able to come back to this anytime soon. Once the new owner's pull-req is merged I will close this pull-req.

I'm not quite happy to take over the PR, but will see what I can do.

I understand that you intend to supplement (rather than replace) the old htop cmdline heuristics (highlight after last slash) with the new heuristics (using stat()).

  • But there could be breakages. For example, consider cmdline=="/A/B C/D" where EXE=="D", but "/A/B C/D" is not stat()able to htop (lack of read-permission in directory "/A/B C"). But if it so happens that "/A/B" exists (whether regular file or not) and stat()able, just "B" would be highlighted - whereas the old heuristics would correctly highlight "D". There could be other corner-cases.

I'm fully aware of this shortcoming of the heuristic, but even then it introduces a great improvement over the previous implementation due to the fact that it at least partially get's the argument splitting right and thus only a small fraction of the actual command line is highlighted. Thus even while potentially the wrong part of the path may be highlighted you'd still only get one small segment highlighted (not the whole rest of the command line).

There is a bit of improvement still possible, such that you could track the exact error returned and thus gain one more level of spaces even for the case of the directory that the file resides in isn't readable to the user itself, as long as there's no further space after that part (i.e. your example). Doable by tracking EACCESS and treating it as "file exists".

  • From the cost PoV, to find the longest stat()able prefix of cmdline, for every space in cmdline there will be a stat() - in the case of chrome and its every thread, there would be 10 stat()'s each (into harddisk eventhough cached) or so. But I guess we are making that compromise for accuracy.
    While present this should be negligible as most paths are accessed repeatedly and there even on busy systems are just so many binaries executed at a time. Thus most calls for stat/lstat/faccessat (with the latter being preferred if available) should just hit the cache of the OS.
  • Also as you have noted, when stat() fails due to lack of permissions (say another user is testing web pages in different chrome version installed in his private directory), the new heuristics can't help - eventhough overall we may expect that accuracy improves to say 99.5% from 99%.

As this heuristic addresses all known real-world cases (Chrome, Keybase) and widely enough covers most potential other cases I'm quite confident that it's quite unlikely that we'll see many more real world cases where this heuristic fails visibly.

Even in your above examples you'll have to have BOTH a command line rewritten to use spaces (only few processes actually do) AND have the executable name contain spaces (even rarer) AND have the stat logic fail to retrieve information. This kind of breakage is likely limited to cases where htop is running with limited permissions. The other case is where cmdline contains arbitrary data (wine as an example, BTW addressed in my heuristic) where information in cmdline does not reflect real paths - there's not much about this case you can do without addressing each and every such program individually.

Important to me is, that the fallback behaviour in case of failure is sensible. And with this new heuristic is very much is: Path splitting by spaces with best effort to resolve the actual path.

Okay, that said, I'll try to rebase this PR onto current master next, so we can address the last issue mentioned (too much magenta).

@BenBE BenBE mentioned this pull request Nov 8, 2020
@gnarendran
Copy link
Copy Markdown
Contributor Author

Agree with what you have said.

Even in your above examples you'll have to have BOTH a command line rewritten to use spaces (only few processes actually do) AND have the executable name contain spaces (even rarer) AND have the stat logic fail to retrieve information. This kind of

The example was indeed contrived to show a corner-case, but one small clarification (though it doesn't make the example more likely): Here I was referring to actual cmdline=="/A/B C/D" (not rewritten one) - i.e. the executable exe=="/A/B C/D" actually exists and is running, but htop is unable to stat() due to dir "/A/B C" not being readable.

Okay, that said, I'll try to rebase this PR onto current master next, so we can address the last issue mentioned (too much magenta).

Super, much appreciated. One parting note: Changes suggested by @fasterit (header changes like uppercasing EXE, COMM, appending "(merged)" to "Command") that are in my local repo but I could not get around to rebase and commit:

handover_patch.txt

Best regards.

@BenBE
Copy link
Copy Markdown
Member

BenBE commented Nov 9, 2020

Okay, that said, I'll try to rebase this PR onto current master next, so we can address the last issue mentioned (too much magenta).

Super, much appreciated. One parting note: Changes suggested by @fasterit (header changes like uppercasing EXE, COMM, appending "(merged)" to "Command") that are in my local repo but I could not get around to rebase and commit:

handover_patch.txt

Best regards.

I'll take a look at them later. Shouldn't be too hard to integrate them. Had to resolve like 4 minor conflicts when rebasing, thus most patches for your branch should work after the rebase as-is.

@BenBE
Copy link
Copy Markdown
Member

BenBE commented Nov 9, 2020

@gnarendran Integrated your patches in my branch.

Now IIRC only the magenta issue should be left.

@fasterit fasterit removed the RFE label Nov 18, 2020
@BenBE
Copy link
Copy Markdown
Member

BenBE commented Nov 24, 2020

Implemented by #305.

Many thanks for your initial patch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-rebase Pull request needs to be rebased and conflicts to be resolved

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants