Skip to main content

analyzed_ipc/
lib.rs

1use std::{
2    env,
3    fs::File,
4    io::{BufRead, BufReader, Write},
5    path::PathBuf,
6};
7
8#[cfg(unix)]
9use std::{
10    fs,
11    os::unix::{
12        fs::{FileTypeExt, PermissionsExt},
13        net::{UnixListener, UnixStream},
14    },
15    path::Path,
16};
17
18#[cfg(windows)]
19use std::{
20    ffi::OsStr,
21    io::Read,
22    os::windows::{
23        ffi::OsStrExt,
24        io::{AsRawHandle, FromRawHandle, OwnedHandle},
25    },
26    ptr,
27    time::{Duration, Instant},
28};
29
30use serde::{Deserialize, Serialize, de::DeserializeOwned};
31use thiserror::Error;
32
33#[cfg(windows)]
34use windows_sys::Win32::{
35    Foundation::{
36        ERROR_BROKEN_PIPE, ERROR_IO_PENDING, ERROR_NO_DATA, ERROR_PIPE_BUSY, ERROR_PIPE_CONNECTED,
37        INVALID_HANDLE_VALUE, WAIT_ABANDONED, WAIT_OBJECT_0,
38    },
39    Storage::FileSystem::{
40        CreateFileW, FILE_FLAG_FIRST_PIPE_INSTANCE, FILE_FLAG_OVERLAPPED, FILE_GENERIC_READ,
41        FILE_GENERIC_WRITE, OPEN_EXISTING, PIPE_ACCESS_DUPLEX, ReadFile, WriteFile,
42    },
43    System::{
44        IO::{GetOverlappedResult, OVERLAPPED},
45        Pipes::{
46            ConnectNamedPipe, CreateNamedPipeW, PIPE_READMODE_BYTE, PIPE_REJECT_REMOTE_CLIENTS,
47            PIPE_TYPE_BYTE, PIPE_UNLIMITED_INSTANCES, PIPE_WAIT, WaitNamedPipeW,
48        },
49        Threading::{CreateEventW, CreateMutexW, INFINITE, ReleaseMutex, WaitForSingleObject},
50    },
51};
52
53pub const PROTOCOL_VERSION: u32 = 1;
54
55pub type Result<T> = std::result::Result<T, IpcError>;
56
57#[cfg(unix)]
58pub type IpcListener = UnixListener;
59
60#[cfg(unix)]
61pub type IpcStream = UnixStream;
62
63#[cfg(windows)]
64pub struct IpcListener {
65    pipe_name: String,
66    pending: File,
67    event: OwnedHandle,
68}
69
70// Synchronous named pipe operations serialize on the file object: a thread
71// blocked in ReadFile holds up a concurrent WriteFile on the same instance,
72// which deadlocks the full duplex LSP stream. All pipe handles are therefore
73// opened overlapped, and every stream carries its own event so reads and
74// writes proceed independently.
75#[cfg(windows)]
76pub struct IpcStream {
77    file: File,
78    event: OwnedHandle,
79}
80
81#[cfg(windows)]
82impl IpcStream {
83    fn new(file: File) -> std::io::Result<Self> {
84        Ok(Self {
85            file,
86            event: create_event()?,
87        })
88    }
89
90    pub fn try_clone(&self) -> std::io::Result<Self> {
91        Self::new(self.file.try_clone()?)
92    }
93
94    fn overlapped(&self) -> OVERLAPPED {
95        let mut overlapped: OVERLAPPED = unsafe { std::mem::zeroed() };
96        overlapped.hEvent = self.event.as_raw_handle();
97        overlapped
98    }
99
100    fn finish(&self, overlapped: &mut OVERLAPPED, started: i32) -> std::io::Result<usize> {
101        if started == 0 {
102            let error = std::io::Error::last_os_error();
103            match error.raw_os_error() {
104                Some(code) if code == ERROR_IO_PENDING as i32 => {}
105                Some(code) if code == ERROR_BROKEN_PIPE as i32 => return Ok(0),
106                _ => return Err(error),
107            }
108        }
109
110        let mut transferred = 0;
111        let completed = unsafe {
112            GetOverlappedResult(self.file.as_raw_handle(), overlapped, &mut transferred, 1)
113        };
114        if completed == 0 {
115            let error = std::io::Error::last_os_error();
116            if error.raw_os_error() == Some(ERROR_BROKEN_PIPE as i32) {
117                return Ok(0);
118            }
119
120            return Err(error);
121        }
122
123        Ok(transferred as usize)
124    }
125}
126
127#[cfg(windows)]
128impl Read for IpcStream {
129    fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {
130        let mut overlapped = self.overlapped();
131        let started = unsafe {
132            ReadFile(
133                self.file.as_raw_handle(),
134                buffer.as_mut_ptr(),
135                buffer.len() as u32,
136                ptr::null_mut(),
137                &mut overlapped,
138            )
139        };
140
141        self.finish(&mut overlapped, started)
142    }
143}
144
145#[cfg(windows)]
146impl Write for IpcStream {
147    fn write(&mut self, buffer: &[u8]) -> std::io::Result<usize> {
148        let mut overlapped = self.overlapped();
149        let started = unsafe {
150            WriteFile(
151                self.file.as_raw_handle(),
152                buffer.as_ptr(),
153                buffer.len() as u32,
154                ptr::null_mut(),
155                &mut overlapped,
156            )
157        };
158
159        self.finish(&mut overlapped, started)
160    }
161
162    fn flush(&mut self) -> std::io::Result<()> {
163        Ok(())
164    }
165}
166
167#[cfg(windows)]
168fn create_event() -> std::io::Result<OwnedHandle> {
169    let event = unsafe { CreateEventW(ptr::null(), 1, 0, ptr::null()) };
170    if event.is_null() {
171        return Err(std::io::Error::last_os_error());
172    }
173
174    Ok(unsafe { OwnedHandle::from_raw_handle(event) })
175}
176
177#[derive(Debug, Error)]
178pub enum IpcError {
179    #[error("{0}")]
180    Io(#[from] std::io::Error),
181    #[error("{0}")]
182    Json(#[from] serde_json::Error),
183    #[error("socket path is occupied by a non-socket file: {0}")]
184    OccupiedSocketPath(PathBuf),
185    #[error("{0} is not set")]
186    MissingEnvironment(&'static str),
187    #[error("{0}")]
188    Protocol(String),
189}
190
191#[cfg(unix)]
192#[derive(Clone, Debug, Serialize)]
193pub struct RuntimePaths {
194    pub runtime_dir: PathBuf,
195    pub socket_path: PathBuf,
196}
197
198#[cfg(windows)]
199#[derive(Clone, Debug, Serialize)]
200pub struct RuntimePaths {
201    pub pipe_name: String,
202    pub startup_mutex_name: String,
203}
204
205#[cfg(unix)]
206impl RuntimePaths {
207    pub fn discover() -> Result<Self> {
208        let runtime_dir = runtime_root()?.join("analyzed");
209
210        Ok(Self {
211            socket_path: runtime_dir.join("daemon.sock"),
212            runtime_dir,
213        })
214    }
215
216    pub fn ensure_runtime_dir(&self) -> Result<()> {
217        fs::create_dir_all(&self.runtime_dir)?;
218        fs::set_permissions(&self.runtime_dir, fs::Permissions::from_mode(0o700))?;
219        Ok(())
220    }
221}
222
223#[cfg(windows)]
224impl RuntimePaths {
225    pub fn discover() -> Result<Self> {
226        let username =
227            env::var("USERNAME").map_err(|_| IpcError::MissingEnvironment("USERNAME"))?;
228
229        Ok(Self {
230            pipe_name: format!(r"\\.\pipe\analyzed.{username}"),
231            startup_mutex_name: format!(r"Global\analyzed.{username}.startup"),
232        })
233    }
234}
235
236#[derive(Debug, Serialize, Deserialize)]
237pub struct ClientInfo {
238    pub protocol_version: u32,
239    pub client_version: String,
240}
241
242impl ClientInfo {
243    pub fn current() -> Self {
244        Self {
245            protocol_version: PROTOCOL_VERSION,
246            client_version: env!("CARGO_PKG_VERSION").to_owned(),
247        }
248    }
249}
250
251#[derive(Debug, Serialize, Deserialize)]
252#[serde(tag = "type", content = "payload", rename_all = "snake_case")]
253pub enum DaemonRequest {
254    Hello(ClientInfo),
255    Lsp(ClientInfo),
256    Stop(ClientInfo),
257}
258
259impl DaemonRequest {
260    pub fn hello() -> Self {
261        Self::Hello(ClientInfo::current())
262    }
263
264    pub fn stop() -> Self {
265        Self::Stop(ClientInfo::current())
266    }
267
268    pub fn lsp() -> Self {
269        Self::Lsp(ClientInfo::current())
270    }
271
272    pub fn client_info(&self) -> &ClientInfo {
273        match self {
274            Self::Hello(client) | Self::Lsp(client) | Self::Stop(client) => client,
275        }
276    }
277}
278
279#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
280pub struct BackendKey {
281    pub shared_world: SharedWorldKey,
282    pub workspace_view: WorkspaceViewKey,
283}
284
285#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
286pub struct SharedWorldKey {
287    pub rust_analyzer_version: String,
288    pub toolchain: Option<String>,
289    pub sysroot: Option<String>,
290    pub cargo_target: Option<String>,
291    pub config: SharedWorldConfigKey,
292    pub load: SharedWorldLoadKey,
293}
294
295#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
296pub struct SharedWorldConfigKey {
297    pub cargo: CargoConfigKey,
298}
299
300#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
301pub struct CargoConfigKey {
302    pub all_targets: bool,
303    pub features: String,
304    pub target: Option<String>,
305    pub sysroot: Option<String>,
306    pub sysroot_src: Option<String>,
307    pub rustc_source: Option<String>,
308    pub extra_includes: Vec<String>,
309    pub cfg_overrides: String,
310    pub wrap_rustc_in_build_scripts: bool,
311    pub invocation_strategy: String,
312    pub run_build_script_command: String,
313    pub extra_args: Vec<String>,
314    pub extra_env: Vec<(String, Option<String>)>,
315    pub target_dir_config: String,
316    pub set_test: bool,
317    pub no_deps: bool,
318    pub metadata_extra_args: Vec<String>,
319}
320
321#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
322pub struct SharedWorldLoadKey {
323    pub load_out_dirs_from_check: bool,
324    pub proc_macro_server: ProcMacroServerKey,
325    pub prefill_caches: bool,
326    pub num_worker_threads: u16,
327    pub proc_macro_processes: u16,
328}
329
330#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
331pub enum ProcMacroServerKey {
332    None,
333    Sysroot,
334    Explicit(String),
335}
336
337#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
338pub struct WorkspaceViewKey {
339    pub workspace_roots: Vec<String>,
340    pub analysis: AnalysisConfigKey,
341}
342
343#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
344pub struct AnalysisConfigKey {
345    pub initialization_options: Option<String>,
346    pub workspace_configuration: Option<String>,
347}
348
349#[derive(Clone, Debug, Serialize, Deserialize)]
350pub struct BackendSnapshot {
351    pub key: BackendKey,
352    pub client_sessions: usize,
353    pub overlay_sessions: usize,
354    pub overlay_files: usize,
355    pub workspace_loads: Vec<WorkspaceSnapshot>,
356}
357
358#[derive(Clone, Debug, Serialize, Deserialize)]
359pub struct WorkspaceSnapshot {
360    pub root: String,
361    pub manifest: String,
362    pub packages: usize,
363    pub files: usize,
364    pub proc_macro_server: bool,
365}
366
367#[derive(Clone, Debug, Serialize, Deserialize)]
368pub struct DaemonSnapshot {
369    pub pid: u32,
370    pub started_at_unix_seconds: u64,
371    pub client_sessions: usize,
372    pub backend_sessions: Vec<BackendSnapshot>,
373    pub workspaces: usize,
374    pub workspace_loads: Vec<WorkspaceSnapshot>,
375}
376
377#[derive(Clone, Debug, Serialize, Deserialize)]
378pub struct Hello {
379    pub ok: bool,
380    pub pid: u32,
381    pub protocol_version: u32,
382    pub daemon_version: String,
383    pub rust_analyzer_version: String,
384    pub capabilities: Vec<String>,
385    pub state: Option<DaemonSnapshot>,
386}
387
388impl Hello {
389    pub fn with_state(state: DaemonSnapshot, rust_analyzer_version: String) -> Self {
390        Self {
391            ok: true,
392            pid: state.pid,
393            protocol_version: PROTOCOL_VERSION,
394            daemon_version: env!("CARGO_PKG_VERSION").to_owned(),
395            rust_analyzer_version,
396            capabilities: vec!["lsp".to_owned()],
397            state: Some(state),
398        }
399    }
400}
401
402#[derive(Clone, Debug, Serialize, Deserialize)]
403pub struct Stop {
404    pub accepted: bool,
405    pub pid: u32,
406}
407
408#[derive(Clone, Debug, Serialize, Deserialize)]
409pub struct LspSession {
410    pub accepted: bool,
411    pub pid: u32,
412}
413
414#[derive(Clone, Debug, Serialize, Deserialize)]
415pub struct ProtocolError {
416    pub message: String,
417}
418
419#[derive(Clone, Debug, Serialize, Deserialize)]
420#[serde(tag = "type", content = "payload", rename_all = "snake_case")]
421pub enum DaemonResponse {
422    Hello(Hello),
423    Lsp(LspSession),
424    Stop(Stop),
425    Error(ProtocolError),
426}
427
428#[cfg(unix)]
429pub struct StartupLock {
430    _file: File,
431}
432
433#[cfg(unix)]
434impl StartupLock {
435    pub fn acquire(paths: &RuntimePaths) -> Result<Self> {
436        paths.ensure_runtime_dir()?;
437        let file = File::open(&paths.runtime_dir)?;
438        file.lock()?;
439
440        Ok(Self { _file: file })
441    }
442}
443
444#[cfg(windows)]
445pub struct StartupLock {
446    mutex: OwnedHandle,
447}
448
449#[cfg(windows)]
450impl StartupLock {
451    pub fn acquire(paths: &RuntimePaths) -> Result<Self> {
452        let name = wide_null(&paths.startup_mutex_name);
453        let handle = unsafe { CreateMutexW(ptr::null(), 0, name.as_ptr()) };
454        if handle.is_null() {
455            return Err(std::io::Error::last_os_error().into());
456        }
457
458        let mutex = unsafe { OwnedHandle::from_raw_handle(handle) };
459        match unsafe { WaitForSingleObject(mutex.as_raw_handle(), INFINITE) } {
460            WAIT_OBJECT_0 | WAIT_ABANDONED => Ok(Self { mutex }),
461            _ => Err(std::io::Error::last_os_error().into()),
462        }
463    }
464}
465
466#[cfg(windows)]
467impl Drop for StartupLock {
468    fn drop(&mut self) {
469        unsafe { ReleaseMutex(self.mutex.as_raw_handle()) };
470    }
471}
472
473pub fn connect_hello(paths: &RuntimePaths) -> Result<Hello> {
474    match request(paths, &DaemonRequest::hello())? {
475        DaemonResponse::Hello(hello) => Ok(hello),
476        DaemonResponse::Error(error) => Err(IpcError::Protocol(error.message)),
477        response => Err(IpcError::Protocol(format!(
478            "unexpected daemon response: {response:?}"
479        ))),
480    }
481}
482
483pub fn request_stop(paths: &RuntimePaths) -> Result<Stop> {
484    match request(paths, &DaemonRequest::stop())? {
485        DaemonResponse::Stop(stop) => Ok(stop),
486        DaemonResponse::Error(error) => Err(IpcError::Protocol(error.message)),
487        response => Err(IpcError::Protocol(format!(
488            "unexpected daemon response: {response:?}"
489        ))),
490    }
491}
492
493pub fn connect_lsp_session(paths: &RuntimePaths) -> Result<IpcStream> {
494    let mut stream = connect_stream(paths)?;
495    write_json_line(&mut stream, &DaemonRequest::lsp())?;
496
497    match read_json_line(&mut stream)? {
498        DaemonResponse::Lsp(session) if session.accepted => Ok(stream),
499        DaemonResponse::Lsp(session) => Err(IpcError::Protocol(format!(
500            "daemon rejected lsp session for pid {}",
501            session.pid
502        ))),
503        DaemonResponse::Error(error) => Err(IpcError::Protocol(error.message)),
504        response => Err(IpcError::Protocol(format!(
505            "unexpected daemon response: {response:?}"
506        ))),
507    }
508}
509
510pub fn request(paths: &RuntimePaths, request: &DaemonRequest) -> Result<DaemonResponse> {
511    let mut stream = connect_stream(paths)?;
512    write_json_line(&mut stream, request)?;
513    read_json_line(&mut stream)
514}
515
516#[cfg(unix)]
517pub fn bind_listener(paths: &RuntimePaths) -> Result<IpcListener> {
518    paths.ensure_runtime_dir()?;
519    remove_stale_socket(&paths.socket_path)?;
520
521    Ok(UnixListener::bind(&paths.socket_path)?)
522}
523
524#[cfg(windows)]
525pub fn bind_listener(paths: &RuntimePaths) -> Result<IpcListener> {
526    Ok(IpcListener {
527        pending: create_pipe(&paths.pipe_name, true)?,
528        pipe_name: paths.pipe_name.clone(),
529        event: create_event()?,
530    })
531}
532
533#[cfg(unix)]
534pub fn accept_client(listener: &mut IpcListener) -> Result<IpcStream> {
535    let (stream, _) = listener.accept()?;
536    Ok(stream)
537}
538
539#[cfg(windows)]
540pub fn accept_client(listener: &mut IpcListener) -> Result<IpcStream> {
541    listener.accept()
542}
543
544pub fn read_json_line<T>(stream: &mut IpcStream) -> Result<T>
545where
546    T: DeserializeOwned,
547{
548    let mut reader = BufReader::new(stream.try_clone()?);
549    let mut line = String::new();
550    reader.read_line(&mut line)?;
551
552    Ok(serde_json::from_str(&line)?)
553}
554
555pub fn write_json_line<T>(stream: &mut IpcStream, value: &T) -> Result<()>
556where
557    T: Serialize,
558{
559    serde_json::to_writer(&mut *stream, value)?;
560    stream.write_all(b"\n")?;
561    stream.flush()?;
562
563    Ok(())
564}
565
566#[cfg(unix)]
567fn connect_stream(paths: &RuntimePaths) -> Result<IpcStream> {
568    Ok(UnixStream::connect(&paths.socket_path)?)
569}
570
571#[cfg(windows)]
572fn connect_stream(paths: &RuntimePaths) -> Result<IpcStream> {
573    connect_pipe(&paths.pipe_name)
574}
575
576#[cfg(unix)]
577fn remove_stale_socket(path: &Path) -> Result<()> {
578    if path.try_exists()? {
579        let file_type = fs::symlink_metadata(path)?.file_type();
580        if !file_type.is_socket() {
581            return Err(IpcError::OccupiedSocketPath(path.to_path_buf()));
582        }
583
584        fs::remove_file(path)?;
585    }
586
587    Ok(())
588}
589
590#[cfg(target_os = "macos")]
591fn runtime_root() -> Result<PathBuf> {
592    runtime_env("TMPDIR")
593}
594
595#[cfg(all(unix, not(target_os = "macos")))]
596fn runtime_root() -> Result<PathBuf> {
597    runtime_env("XDG_RUNTIME_DIR")
598}
599
600#[cfg(unix)]
601fn runtime_env(name: &'static str) -> Result<PathBuf> {
602    env::var_os(name)
603        .filter(|path| !path.is_empty())
604        .map(PathBuf::from)
605        .ok_or(IpcError::MissingEnvironment(name))
606}
607
608#[cfg(windows)]
609impl IpcListener {
610    fn accept(&mut self) -> Result<IpcStream> {
611        loop {
612            match connect_pending_pipe(&self.pending, &self.event) {
613                Ok(()) => {
614                    let next = create_pipe(&self.pipe_name, false)?;
615                    return Ok(IpcStream::new(std::mem::replace(&mut self.pending, next))?);
616                }
617                // The client connected and vanished before we picked the
618                // instance up. Stand up a replacement first so the pipe name
619                // never disappears, then retire the dead instance.
620                Err(IpcError::Io(error)) if error.raw_os_error() == Some(ERROR_NO_DATA as i32) => {
621                    let next = create_pipe(&self.pipe_name, false)?;
622                    self.pending = next;
623                }
624                Err(error) => return Err(error),
625            }
626        }
627    }
628}
629
630#[cfg(windows)]
631fn create_pipe(pipe_name: &str, first_instance: bool) -> Result<File> {
632    let pipe_name = wide_null(pipe_name);
633    let first_instance = if first_instance {
634        FILE_FLAG_FIRST_PIPE_INSTANCE
635    } else {
636        0
637    };
638    let handle = unsafe {
639        CreateNamedPipeW(
640            pipe_name.as_ptr(),
641            PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED | first_instance,
642            PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT | PIPE_REJECT_REMOTE_CLIENTS,
643            PIPE_UNLIMITED_INSTANCES,
644            65_536,
645            65_536,
646            0,
647            ptr::null_mut(),
648        )
649    };
650    if handle == INVALID_HANDLE_VALUE {
651        return Err(std::io::Error::last_os_error().into());
652    }
653
654    Ok(unsafe { File::from_raw_handle(handle) })
655}
656
657#[cfg(windows)]
658fn connect_pending_pipe(file: &File, event: &OwnedHandle) -> Result<()> {
659    let mut overlapped: OVERLAPPED = unsafe { std::mem::zeroed() };
660    overlapped.hEvent = event.as_raw_handle();
661
662    let connected = unsafe { ConnectNamedPipe(file.as_raw_handle(), &mut overlapped) };
663    if connected != 0 {
664        return Ok(());
665    }
666
667    let error = std::io::Error::last_os_error();
668    match error.raw_os_error() {
669        Some(code) if code == ERROR_PIPE_CONNECTED as i32 => Ok(()),
670        Some(code) if code == ERROR_IO_PENDING as i32 => {
671            let mut transferred = 0;
672            let completed = unsafe {
673                GetOverlappedResult(file.as_raw_handle(), &overlapped, &mut transferred, 1)
674            };
675            if completed == 0 {
676                return Err(std::io::Error::last_os_error().into());
677            }
678
679            Ok(())
680        }
681        _ => Err(error.into()),
682    }
683}
684
685#[cfg(windows)]
686fn connect_pipe(pipe_name: &str) -> Result<IpcStream> {
687    let wide_name = wide_null(pipe_name);
688    let deadline = Instant::now() + Duration::from_secs(1);
689
690    loop {
691        let handle = unsafe {
692            CreateFileW(
693                wide_name.as_ptr(),
694                FILE_GENERIC_READ | FILE_GENERIC_WRITE,
695                0,
696                ptr::null_mut(),
697                OPEN_EXISTING,
698                FILE_FLAG_OVERLAPPED,
699                ptr::null_mut(),
700            )
701        };
702        if handle != INVALID_HANDLE_VALUE {
703            return Ok(IpcStream::new(unsafe { File::from_raw_handle(handle) })?);
704        }
705
706        let error = std::io::Error::last_os_error();
707        if error.raw_os_error() != Some(ERROR_PIPE_BUSY as i32) {
708            return Err(error.into());
709        }
710
711        // Every instance is momentarily taken; wait for the daemon to stand
712        // up the next one. This is the documented client side of the named
713        // pipe connect handshake.
714        let remaining = deadline.saturating_duration_since(Instant::now());
715        if remaining.is_zero() {
716            return Err(error.into());
717        }
718
719        unsafe { WaitNamedPipeW(wide_name.as_ptr(), remaining.as_millis().max(1) as u32) };
720    }
721}
722
723#[cfg(windows)]
724fn wide_null(value: &str) -> Vec<u16> {
725    OsStr::new(value)
726        .encode_wide()
727        .chain(std::iter::once(0))
728        .collect()
729}