Skip to main content

proc_cli/commands/
on.rs

1//! `proc on` - Port/process lookup
2//!
3//! Usage:
4//!   proc on :3000              # What process is on port 3000?
5//!   proc on :3000 :8080        # What's on several ports? (separate arguments)
6//!   proc on 1234               # What ports is PID 1234 listening on?
7//!   proc on node               # What ports are the processes named node listening on?
8//!   proc on node --cmd         # ... or with node anywhere in their command line
9//!   proc on node --in .        # Node processes in cwd and their ports
10//!
11//! Commas still work (`proc on :3000,:8080`) but are deprecated.
12
13use crate::core::{
14    find_ports_for_pid, flatten_targets, parse_target, resolve_in_dir, resolve_target, NameMatch,
15    PortInfo, Process, TargetType,
16};
17use crate::error::{ExitCode, ProcError, Result};
18use crate::ui::{format_duration, format_memory, Printer};
19use clap::Args;
20use colored::*;
21use serde::Serialize;
22use std::io::Write;
23
24/// What println! would say if stdout were closed
25const WRITE_FAILED: &str = "failed printing to stdout";
26
27/// Show what's on a port, or what ports a process is on
28#[derive(Args, Debug)]
29pub struct OnCommand {
30    /// Target(s): :port, PID, or process name (separate arguments for several)
31    #[arg(required = true, num_args = 1.., value_parser = crate::core::validate_target_arg)]
32    pub target: Vec<String>,
33
34    /// Filter by directory; takes the next word as the directory unless it starts with '-'
35    /// (bare --in is the current directory, --in=DIR is explicit)
36    #[arg(long = "in", short = 'i', num_args = 0..=1, default_missing_value = ".")]
37    pub in_dir: Option<String>,
38
39    /// Filter by process name
40    #[arg(long = "by", short = 'b')]
41    pub by_name: Option<String>,
42
43    /// Match name targets and --by as a substring of the process name or full command line (like pkill -f)
44    #[arg(long)]
45    pub cmd: bool,
46
47    /// Output as JSON
48    #[arg(long, short = 'j')]
49    pub json: bool,
50
51    /// Show verbose output (full command line)
52    #[arg(long, short = 'v')]
53    pub verbose: bool,
54}
55
56impl OnCommand {
57    /// Executes the on command, performing bidirectional port/process lookup.
58    pub fn execute(&self) -> Result<()> {
59        let targets = flatten_targets(&self.target);
60
61        // For single target, use original behavior
62        if targets.len() == 1 {
63            return match parse_target(&targets[0]) {
64                TargetType::Port(port) => self.show_process_on_port(port),
65                TargetType::Pid(pid) => self.show_ports_for_pid(pid),
66                TargetType::Name(name) => self.show_ports_for_name(&name),
67            };
68        }
69
70        if self.json {
71            return self.show_multi_json(&targets);
72        }
73
74        // Multi-target handling
75        let mut not_found = Vec::new();
76
77        for target in &targets {
78            match parse_target(target) {
79                TargetType::Port(port) => {
80                    if let Err(e) = self.show_process_on_port(port) {
81                        println!("{} Port {}: {}", "⚠".yellow(), port, e);
82                        not_found.push(target.clone());
83                    }
84                }
85                TargetType::Pid(pid) => {
86                    if let Err(e) = self.show_ports_for_pid(pid) {
87                        println!("{} PID {}: {}", "⚠".yellow(), pid, e);
88                        not_found.push(target.clone());
89                    }
90                }
91                TargetType::Name(ref name) => {
92                    if let Err(e) = self.show_ports_for_name(name) {
93                        println!("{} '{}': {}", "⚠".yellow(), name, e);
94                        not_found.push(target.clone());
95                    }
96                }
97            }
98        }
99
100        if not_found.len() == targets.len() {
101            // Each target already printed its own warning
102            return Err(ProcError::reported(
103                ExitCode::NotFound,
104                format!("No process found matching '{}'", self.target.join(" ")),
105            ));
106        }
107
108        Ok(())
109    }
110
111    /// Multi-target JSON: one envelope with a result per matched process
112    fn show_multi_json(&self, targets: &[String]) -> Result<()> {
113        let mut results: Vec<TargetResult> = Vec::new();
114        let mut not_found: Vec<String> = Vec::new();
115
116        for target in targets {
117            let found = match parse_target(target) {
118                TargetType::Port(port) => self.lookup_port(port).map(port_hits),
119                TargetType::Pid(pid) => self.lookup_pid(pid).map(|found| vec![found]),
120                TargetType::Name(ref name) => self.lookup_name(name),
121            };
122
123            match found {
124                Ok(found) if !found.is_empty() => {
125                    results.extend(found.into_iter().map(|(process, ports)| TargetResult {
126                        target: target.clone(),
127                        process,
128                        ports,
129                    }))
130                }
131                _ => not_found.push(target.clone()),
132            }
133        }
134
135        if results.is_empty() {
136            return Err(ProcError::ProcessNotFound(self.target.join(" ")));
137        }
138
139        Printer::from_flags(true, self.verbose).print_json(&MultiTargetOutput {
140            action: "on",
141            success: true,
142            count: results.len(),
143            results,
144            not_found,
145        });
146
147        Ok(())
148    }
149
150    /// Check if process matches --in filter
151    fn matches_in_filter(&self, proc: &Process) -> bool {
152        if let Some(ref dir_path) = resolve_in_dir(&self.in_dir) {
153            if let Some(ref proc_cwd) = proc.cwd {
154                crate::core::filters::cwd_within(proc_cwd, dir_path)
155            } else {
156                false
157            }
158        } else {
159            true
160        }
161    }
162
163    /// Check if process matches --by filter
164    fn matches_by_filter(&self, proc: &Process) -> bool {
165        if let Some(ref name) = self.by_name {
166            crate::core::matches_by_filter(proc, name, NameMatch::from_flag(self.cmd))
167        } else {
168            true
169        }
170    }
171
172    /// Check if process matches all filters (--in and --by)
173    fn matches_filters(&self, proc: &Process) -> bool {
174        self.matches_in_filter(proc) && self.matches_by_filter(proc)
175    }
176
177    /// Find every listener on a port with its process, applying --in/--by
178    fn lookup_port(&self, port: u16) -> Result<Vec<(PortInfo, Option<Process>)>> {
179        let listeners = PortInfo::holders_of(port)?;
180        if listeners.is_empty() {
181            return Err(ProcError::PortNotFound(port));
182        }
183
184        let mut holders = Vec::new();
185        for listener in listeners {
186            let process = Process::find_by_pid(listener.pid)?;
187            holders.push((listener, process));
188        }
189        self.filter_holders(port, holders)
190    }
191
192    /// Apply --in/--by to the holders of a port and take the CPU reading of
193    /// the ones that stay, all in one wait. A holder whose process is `None`
194    /// ended after its port was listed, and is kept without one. When the
195    /// filters leave no holder the port counts as not found.
196    fn filter_holders(
197        &self,
198        port: u16,
199        holders: Vec<(PortInfo, Option<Process>)>,
200    ) -> Result<Vec<(PortInfo, Option<Process>)>> {
201        let mut kept: Vec<(PortInfo, Option<Process>)> = holders
202            .into_iter()
203            .filter(|(_, process)| process.as_ref().is_none_or(|p| self.matches_filters(p)))
204            .collect();
205        if kept.is_empty() {
206            return Err(ProcError::FilteredOut {
207                target: format!(":{port}"),
208                reason: "process not in specified directory".to_string(),
209            });
210        }
211
212        // Real CPU reading for what's shown
213        let mut live: Vec<Process> = kept.iter().filter_map(|(_, p)| p.clone()).collect();
214        Process::measure_cpu(&mut live);
215        let mut live = live.into_iter();
216        for (_, slot) in kept.iter_mut() {
217            if slot.is_some() {
218                *slot = live.next();
219            }
220        }
221        Ok(kept)
222    }
223
224    /// Show what process is on a specific port
225    fn show_process_on_port(&self, port: u16) -> Result<()> {
226        let holders = self.lookup_port(port)?;
227
228        if self.json {
229            let printer = Printer::from_flags(true, self.verbose);
230            let (first, _) = &holders[0];
231            let processes: Vec<&Process> = holders.iter().filter_map(|(_, p)| p.as_ref()).collect();
232            let output = PortLookupOutput {
233                action: "on",
234                query_type: "port_to_process",
235                success: true,
236                port: Some(first.port),
237                protocol: Some(format!("{:?}", first.protocol).to_lowercase()),
238                address: first.address.clone(),
239                // the first holder, as before `processes` existed
240                process: processes.first().copied(),
241                processes: Some(processes),
242                ports: None,
243            };
244            printer.print_json(&output);
245        } else {
246            self.print_process_on_port(&mut std::io::stdout(), port, &holders)
247                .expect(WRITE_FAILED);
248        }
249
250        Ok(())
251    }
252
253    /// Find a process by PID and its listening ports, applying --in/--by
254    fn lookup_pid(&self, pid: u32) -> Result<(Process, Vec<PortInfo>)> {
255        let mut process = Process::find_by_pid(pid)?
256            .ok_or_else(|| ProcError::ProcessNotFound(pid.to_string()))?;
257        // Real CPU reading for what's shown
258        Process::measure_cpu(std::slice::from_mut(&mut process));
259
260        // Apply --in and --by filters if present
261        if !self.matches_filters(&process) {
262            return Err(ProcError::FilteredOut {
263                target: pid.to_string(),
264                reason: "not in specified directory".to_string(),
265            });
266        }
267
268        let ports = find_ports_for_pid(pid)?;
269        Ok((process, ports))
270    }
271
272    /// Show what ports a PID is listening on
273    fn show_ports_for_pid(&self, pid: u32) -> Result<()> {
274        let (process, ports) = self.lookup_pid(pid)?;
275
276        if self.json {
277            let printer = Printer::from_flags(true, self.verbose);
278            let output = PortLookupOutput {
279                action: "on",
280                query_type: "process_to_ports",
281                success: true,
282                port: None,
283                protocol: None,
284                address: None,
285                process: Some(&process),
286                processes: None,
287                ports: Some(&ports),
288            };
289            printer.print_json(&output);
290        } else {
291            self.print_ports_for_process(&mut std::io::stdout(), &process, &ports)
292                .expect(WRITE_FAILED);
293        }
294
295        Ok(())
296    }
297
298    /// Find processes by name and their listening ports, applying --in/--by
299    fn lookup_name(&self, name: &str) -> Result<Vec<(Process, Vec<PortInfo>)>> {
300        // A name that matches nothing is an error here, never an empty list
301        let mut processes = resolve_target(name, NameMatch::from_flag(self.cmd))?;
302
303        // Apply --in and --by filters if present
304        if self.in_dir.is_some() || self.by_name.is_some() {
305            processes.retain(|p| self.matches_filters(p));
306            if processes.is_empty() {
307                return Err(ProcError::FilteredOut {
308                    target: name.to_string(),
309                    reason: "no matches with specified filters".to_string(),
310                });
311            }
312        }
313
314        // Real CPU readings for what's shown, one wait for all of them
315        Process::measure_cpu(&mut processes);
316
317        let mut all_results: Vec<(Process, Vec<PortInfo>)> = Vec::new();
318
319        for proc in processes {
320            let ports = find_ports_for_pid(proc.pid)?;
321            all_results.push((proc, ports));
322        }
323
324        Ok(all_results)
325    }
326
327    /// Show what ports processes with a given name are listening on
328    fn show_ports_for_name(&self, name: &str) -> Result<()> {
329        let all_results = self.lookup_name(name)?;
330
331        if self.json {
332            let printer = Printer::from_flags(true, self.verbose);
333            let results: Vec<_> = all_results
334                .iter()
335                .map(|(proc, ports)| ProcessPortsJson {
336                    process: proc,
337                    ports,
338                })
339                .collect();
340            let output = MultiProcessPortsOutput {
341                action: "on",
342                success: true,
343                count: results.len(),
344                results: &results,
345            };
346            printer.print_json(&output);
347        } else {
348            for (proc, ports) in &all_results {
349                self.print_ports_for_process(&mut std::io::stdout(), proc, ports)
350                    .expect(WRITE_FAILED);
351            }
352        }
353
354        Ok(())
355    }
356
357    /// What is on `port`: the line saying so, then each holder
358    fn print_process_on_port(
359        &self,
360        out: &mut impl Write,
361        port: u16,
362        holders: &[(PortInfo, Option<Process>)],
363    ) -> std::io::Result<()> {
364        let who = if holders.len() > 1 {
365            format!(" {} processes", holders.len())
366        } else {
367            String::new()
368        };
369        writeln!(
370            out,
371            "{} Port {} is used by{}:",
372            "✓".green().bold(),
373            port.to_string().cyan().bold(),
374            who
375        )?;
376        writeln!(out)?;
377
378        for (port_info, process) in holders {
379            self.print_holder(out, port_info, process.as_ref())?;
380        }
381        Ok(())
382    }
383
384    fn print_holder(
385        &self,
386        out: &mut impl Write,
387        port_info: &PortInfo,
388        process: Option<&Process>,
389    ) -> std::io::Result<()> {
390        writeln!(
391            out,
392            "  {} {} (PID {})",
393            "Process:".bright_black(),
394            port_info.process_name.white().bold(),
395            port_info.pid.to_string().cyan()
396        )?;
397
398        if let Some(proc) = process {
399            if let Some(ref cwd) = proc.cwd {
400                writeln!(out, "  {} {}", "Directory:".bright_black(), cwd)?;
401            }
402            if let Some(ref path) = proc.exe_path {
403                writeln!(out, "  {} {}", "Path:".bright_black(), path.bright_black())?;
404            }
405        }
406
407        let addr = port_info.address.as_deref().unwrap_or("*");
408        writeln!(
409            out,
410            "  {} {} on {}",
411            "Listening:".bright_black(),
412            format!("{:?}", port_info.protocol).to_uppercase(),
413            addr
414        )?;
415
416        if let Some(proc) = process {
417            writeln!(
418                out,
419                "  {} {:.1}% CPU, {}",
420                "Resources:".bright_black(),
421                proc.cpu_percent,
422                format_memory(proc.memory_mb)
423            )?;
424
425            if let Some(uptime) = proc.uptime_secs {
426                writeln!(
427                    out,
428                    "  {} {}",
429                    "Uptime:".bright_black(),
430                    format_duration(uptime)
431                )?;
432            }
433
434            if self.verbose {
435                if let Some(ref cmd) = proc.command {
436                    writeln!(
437                        out,
438                        "  {} {}",
439                        "Command:".bright_black(),
440                        cmd.bright_black()
441                    )?;
442                }
443            }
444        }
445
446        writeln!(out)
447    }
448
449    fn print_ports_for_process(
450        &self,
451        out: &mut impl Write,
452        process: &Process,
453        ports: &[PortInfo],
454    ) -> std::io::Result<()> {
455        writeln!(
456            out,
457            "{} {} (PID {}) is listening on:",
458            "✓".green().bold(),
459            process.name.white().bold(),
460            process.pid.to_string().cyan().bold()
461        )?;
462        writeln!(out)?;
463
464        if ports.is_empty() {
465            writeln!(out, "  {} No listening ports", "ℹ".blue())?;
466        } else {
467            for port_info in ports {
468                let addr = port_info.address.as_deref().unwrap_or("*");
469                writeln!(
470                    out,
471                    "  {} :{} ({} on {})",
472                    "→".bright_black(),
473                    port_info.port.to_string().cyan(),
474                    format!("{:?}", port_info.protocol).to_uppercase(),
475                    addr
476                )?;
477            }
478        }
479
480        if process.cwd.is_some() || process.uptime_secs.is_some() {
481            writeln!(out)?;
482        }
483        if let Some(ref cwd) = process.cwd {
484            writeln!(out, "  {} {}", "Directory:".bright_black(), cwd)?;
485        }
486        if let Some(uptime) = process.uptime_secs {
487            writeln!(
488                out,
489                "  {} {}",
490                "Uptime:".bright_black(),
491                format_duration(uptime)
492            )?;
493        }
494
495        if self.verbose {
496            if let Some(ref path) = process.exe_path {
497                writeln!(out, "  {} {}", "Path:".bright_black(), path.bright_black())?;
498            }
499            if let Some(ref cmd) = process.command {
500                writeln!(
501                    out,
502                    "  {} {}",
503                    "Command:".bright_black(),
504                    cmd.bright_black()
505                )?;
506            }
507        }
508
509        writeln!(out)
510    }
511}
512
513/// What a port lookup contributes to the multi-target JSON: each holder's
514/// process with the port it holds. A holder whose process ended after the port
515/// was listed adds nothing.
516fn port_hits(holders: Vec<(PortInfo, Option<Process>)>) -> Vec<(Process, Vec<PortInfo>)> {
517    holders
518        .into_iter()
519        .filter_map(|(port_info, process)| process.map(|p| (p, vec![port_info])))
520        .collect()
521}
522
523#[derive(Serialize)]
524struct PortLookupOutput<'a> {
525    action: &'static str,
526    query_type: &'static str,
527    success: bool,
528    #[serde(skip_serializing_if = "Option::is_none")]
529    port: Option<u16>,
530    #[serde(skip_serializing_if = "Option::is_none")]
531    protocol: Option<String>,
532    #[serde(skip_serializing_if = "Option::is_none")]
533    address: Option<String>,
534    #[serde(skip_serializing_if = "Option::is_none")]
535    process: Option<&'a Process>,
536    /// Every process holding the port (`process` is the first of them)
537    #[serde(skip_serializing_if = "Option::is_none")]
538    processes: Option<Vec<&'a Process>>,
539    #[serde(skip_serializing_if = "Option::is_none")]
540    ports: Option<&'a [PortInfo]>,
541}
542
543#[derive(Serialize)]
544struct ProcessPortsJson<'a> {
545    process: &'a Process,
546    ports: &'a [PortInfo],
547}
548
549#[derive(Serialize)]
550struct TargetResult {
551    target: String,
552    process: Process,
553    ports: Vec<PortInfo>,
554}
555
556#[derive(Serialize)]
557struct MultiTargetOutput {
558    action: &'static str,
559    success: bool,
560    count: usize,
561    results: Vec<TargetResult>,
562    not_found: Vec<String>,
563}
564
565#[derive(Serialize)]
566struct MultiProcessPortsOutput<'a> {
567    action: &'static str,
568    success: bool,
569    count: usize,
570    results: &'a Vec<ProcessPortsJson<'a>>,
571}
572
573#[cfg(test)]
574mod tests {
575    use super::*;
576    use crate::core::{ProcessStatus, Protocol};
577    use crate::test_support::assert_stops_at_a_failed_write;
578
579    fn command(in_dir: Option<&str>) -> OnCommand {
580        OnCommand {
581            target: vec!["x".to_string()],
582            in_dir: in_dir.map(String::from),
583            by_name: None,
584            cmd: false,
585            json: false,
586            verbose: false,
587        }
588    }
589
590    /// A process record with no working directory, which some processes really
591    /// are (a zombie has none on Linux)
592    fn without_cwd() -> Process {
593        Process {
594            pid: 1,
595            name: "x".to_string(),
596            exe_path: None,
597            cwd: None,
598            command: None,
599            argv0: None,
600            raw: None,
601            cpu_percent: 0.0,
602            memory_mb: 0.0,
603            status: ProcessStatus::Sleeping,
604            user: None,
605            parent_pid: None,
606            start_time: None,
607            uptime_secs: None,
608        }
609    }
610
611    fn listener() -> PortInfo {
612        PortInfo {
613            port: 4000,
614            protocol: Protocol::Tcp,
615            pid: 1,
616            process_name: "x".to_string(),
617            address: None,
618        }
619    }
620
621    #[test]
622    fn a_process_with_no_working_directory_fails_in() {
623        assert!(!command(Some("/")).matches_in_filter(&without_cwd()));
624        assert!(command(None).matches_in_filter(&without_cwd()));
625    }
626
627    #[test]
628    fn a_port_whose_process_ended_has_no_process_to_filter() {
629        let holders = command(Some("/"))
630            .filter_holders(4000, vec![(listener(), None)])
631            .unwrap();
632        assert_eq!(holders.len(), 1);
633        assert_eq!(holders[0].0.port, 4000);
634        assert!(holders[0].1.is_none());
635    }
636
637    #[test]
638    fn the_filters_drop_the_holders_they_do_not_match() {
639        let mut elsewhere = without_cwd();
640        elsewhere.pid = 2;
641        elsewhere.cwd = Some("/definitely/not/here".to_string());
642        let mut here = without_cwd();
643        here.pid = 3;
644        here.cwd = Some(
645            std::env::current_dir()
646                .unwrap()
647                .to_string_lossy()
648                .into_owned(),
649        );
650
651        let holders = vec![
652            (listener(), Some(elsewhere)),
653            (listener(), Some(here.clone())),
654            (listener(), None),
655        ];
656        let kept = command(Some("."))
657            .filter_holders(4000, holders.clone())
658            .unwrap();
659        let pids: Vec<_> = kept
660            .iter()
661            .map(|(_, p)| p.as_ref().map(|p| p.pid))
662            .collect();
663        assert_eq!(pids, vec![Some(3), None]);
664
665        // nothing left: the port counts as not found
666        let none = command(Some("/definitely/not/here/either"))
667            .filter_holders(4000, holders[..1].to_vec());
668        assert!(
669            matches!(&none, Err(ProcError::FilteredOut { target, .. }) if target == ":4000"),
670            "{none:?}"
671        );
672    }
673
674    #[test]
675    fn a_port_whose_process_ended_adds_nothing_to_the_json_results() {
676        assert!(port_hits(vec![(listener(), None)]).is_empty());
677        let hits = port_hits(vec![
678            (listener(), Some(without_cwd())),
679            (listener(), None),
680            (listener(), Some(without_cwd())),
681        ]);
682        assert_eq!(hits.len(), 2);
683    }
684
685    /// What a print function writes, with colours off
686    fn printed(print: impl FnOnce(&mut Vec<u8>) -> std::io::Result<()>) -> String {
687        colored::control::set_override(false);
688        let mut out = Vec::new();
689        print(&mut out).unwrap();
690        String::from_utf8(out).unwrap()
691    }
692
693    #[test]
694    fn a_port_without_a_process_still_prints() {
695        // Neither the process nor its details: the address shows as `*`
696        let mut cmd = command(None);
697        cmd.verbose = true;
698        let out = printed(|w| cmd.print_process_on_port(w, 4000, &[(listener(), None)]));
699        assert_eq!(
700            out,
701            "✓ Port 4000 is used by:\n\n  Process: x (PID 1)\n  Listening: TCP on *\n\n"
702        );
703    }
704
705    #[test]
706    fn a_port_with_several_holders_says_so_and_lists_each() {
707        let mut second = listener();
708        second.pid = 2;
709        second.address = Some("::1".to_string());
710        let out = printed(|w| {
711            command(None).print_process_on_port(w, 4000, &[(listener(), None), (second, None)])
712        });
713        assert_eq!(
714            out,
715            "✓ Port 4000 is used by 2 processes:\n\n\
716             \x20 Process: x (PID 1)\n  Listening: TCP on *\n\n\
717             \x20 Process: x (PID 2)\n  Listening: TCP on ::1\n\n"
718        );
719    }
720
721    #[test]
722    fn a_process_missing_its_details_prints_without_them() {
723        let mut cmd = command(None);
724        cmd.verbose = true;
725
726        let out =
727            printed(|w| cmd.print_process_on_port(w, 4000, &[(listener(), Some(without_cwd()))]));
728        assert!(out.contains("Listening: TCP on *\n"), "{out}");
729        assert!(out.contains("Resources: 0.0% CPU"), "{out}");
730        for label in ["Directory:", "Path:", "Uptime:", "Command:"] {
731            assert!(!out.contains(label), "{label} printed for nothing: {out}");
732        }
733
734        let out = printed(|w| cmd.print_ports_for_process(w, &without_cwd(), &[listener()]));
735        assert_eq!(
736            out,
737            "✓ x (PID 1) is listening on:\n\n  → :4000 (TCP on *)\n\n"
738        );
739    }
740
741    /// A process with every detail the printers can show
742    fn detailed() -> Process {
743        let mut p = without_cwd();
744        p.cwd = Some("/work/app".to_string());
745        p.exe_path = Some("/usr/bin/x".to_string());
746        p.command = Some("x --serve".to_string());
747        p.uptime_secs = Some(90);
748        p
749    }
750
751    #[test]
752    fn a_failed_write_ends_the_port_lookup() {
753        let mut cmd = command(None);
754        cmd.verbose = true;
755        let p = detailed();
756        let held = [(listener(), Some(p))];
757        assert_stops_at_a_failed_write(|w| cmd.print_process_on_port(w, 4000, &held));
758        assert_stops_at_a_failed_write(|w| {
759            cmd.print_process_on_port(w, 4000, &[(listener(), None)])
760        });
761    }
762
763    #[test]
764    fn a_failed_write_ends_the_process_lookup() {
765        let mut cmd = command(None);
766        cmd.verbose = true;
767        let p = detailed();
768        assert_stops_at_a_failed_write(|w| cmd.print_ports_for_process(w, &p, &[listener()]));
769        assert_stops_at_a_failed_write(|w| cmd.print_ports_for_process(w, &p, &[]));
770    }
771}