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