Skip to content

Commit d9a5c09

Browse files
authoredMar 19, 2025··
Refactor launch process detection (#236)
1 parent 21872e5 commit d9a5c09

File tree

1 file changed

+188
-49
lines changed

1 file changed

+188
-49
lines changed
 
+188-49
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,206 @@
11
use crate::dotnet::project::ProjectType;
22
use crate::dotnet::solution::Solution;
3+
use crate::Project;
34
use libcnb::data::launch::{Process, ProcessBuilder, ProcessType, ProcessTypeError};
5+
use std::path::PathBuf;
46

57
#[derive(Debug)]
68
pub(crate) enum LaunchProcessDetectionError {
79
ProcessType(ProcessTypeError),
810
}
911

12+
/// Detects processes in a solution's projects
1013
pub(crate) fn detect_solution_processes(
1114
solution: &Solution,
1215
) -> Result<Vec<Process>, LaunchProcessDetectionError> {
1316
solution
1417
.projects
1518
.iter()
16-
.filter(|project| {
17-
matches!(
18-
project.project_type,
19-
ProjectType::ConsoleApplication
20-
| ProjectType::WebApplication
21-
| ProjectType::WorkerService
22-
)
23-
})
24-
.map(|project| {
25-
let executable_path = project
19+
.filter_map(|project| project_launch_process(solution, project))
20+
.collect::<Result<_, _>>()
21+
}
22+
23+
/// Determines if a project should have a launchable process and constructs it
24+
fn project_launch_process(
25+
solution: &Solution,
26+
project: &Project,
27+
) -> Option<Result<Process, LaunchProcessDetectionError>> {
28+
if !matches!(
29+
project.project_type,
30+
ProjectType::ConsoleApplication | ProjectType::WebApplication | ProjectType::WorkerService
31+
) {
32+
return None;
33+
}
34+
let relative_executable_path = relative_executable_path(solution, project);
35+
36+
let mut command = format!(
37+
"cd {}; ./{}",
38+
relative_executable_path
39+
.parent()
40+
.expect("Path to always have a parent directory")
41+
.display(),
42+
relative_executable_path
43+
.file_name()
44+
.expect("Path to never terminate in `..`")
45+
.to_string_lossy()
46+
);
47+
48+
if project.project_type == ProjectType::WebApplication {
49+
command.push_str(" --urls http://*:$PORT");
50+
}
51+
52+
Some(
53+
project_process_type(project).map(|process_type| {
54+
ProcessBuilder::new(process_type, ["bash", "-c", &command]).build()
55+
}),
56+
)
57+
}
58+
59+
fn project_process_type(project: &Project) -> Result<ProcessType, LaunchProcessDetectionError> {
60+
project
61+
.assembly_name
62+
.parse::<ProcessType>()
63+
.map_err(LaunchProcessDetectionError::ProcessType)
64+
}
65+
66+
/// Returns the (expected) relative executable path from the solution's parent directory
67+
fn relative_executable_path(solution: &Solution, project: &Project) -> PathBuf {
68+
project_executable_path(project)
69+
.strip_prefix(
70+
solution
2671
.path
2772
.parent()
28-
.expect("Project file should always have a parent directory")
29-
.join("bin")
30-
.join("publish")
31-
.join(&project.assembly_name);
32-
33-
let relative_executable_path = executable_path
34-
.strip_prefix(
35-
solution
36-
.path
37-
.parent()
38-
.expect("Solution path to have a parent"),
39-
)
40-
.expect("Project to be nested in solution parent directory");
41-
42-
let mut command = format!(
43-
"cd {}; ./{}",
44-
relative_executable_path
45-
.parent()
46-
.expect("Path to always have a parent directory")
47-
.display(),
48-
relative_executable_path
49-
.file_name()
50-
.expect("Path to never terminate in `..`")
51-
.to_string_lossy()
52-
);
53-
54-
if project.project_type == ProjectType::WebApplication {
55-
command.push_str(" --urls http://*:$PORT");
56-
}
57-
58-
project
59-
.assembly_name
60-
.parse::<ProcessType>()
61-
.map_err(LaunchProcessDetectionError::ProcessType)
62-
.map(|process_type| {
63-
ProcessBuilder::new(process_type, ["bash", "-c", &command]).build()
64-
})
65-
})
66-
.collect::<Result<_, _>>()
73+
.expect("Solution path to have a parent"),
74+
)
75+
.expect("Project to be nested in solution parent directory")
76+
.to_path_buf()
77+
}
78+
79+
/// Returns the (expected) absolute path to the project's compiled executable
80+
fn project_executable_path(project: &Project) -> PathBuf {
81+
project
82+
.path
83+
.parent()
84+
.expect("Project file should always have a parent directory")
85+
.join("bin")
86+
.join("publish")
87+
.join(&project.assembly_name)
88+
}
89+
90+
#[cfg(test)]
91+
mod tests {
92+
use super::*;
93+
use libcnb::data::launch::{Process, WorkingDirectory};
94+
use libcnb::data::process_type;
95+
use std::path::PathBuf;
96+
97+
#[test]
98+
fn test_detect_solution_processes_web_app() {
99+
let solution = Solution {
100+
path: PathBuf::from("/tmp/foo.sln"),
101+
projects: vec![Project {
102+
path: PathBuf::from("/tmp/bar/bar.csproj"),
103+
target_framework: "net9.0".to_string(),
104+
project_type: ProjectType::WebApplication,
105+
assembly_name: "bar".to_string(),
106+
}],
107+
};
108+
109+
let expected_processes = vec![Process {
110+
r#type: process_type!("bar"),
111+
command: vec![
112+
"bash".to_string(),
113+
"-c".to_string(),
114+
"cd bar/bin/publish; ./bar --urls http://*:$PORT".to_string(),
115+
],
116+
args: vec![],
117+
default: false,
118+
working_directory: WorkingDirectory::App,
119+
}];
120+
121+
assert_eq!(
122+
detect_solution_processes(&solution).unwrap(),
123+
expected_processes
124+
);
125+
}
126+
127+
#[test]
128+
fn test_detect_solution_processes_console_app() {
129+
let solution = Solution {
130+
path: PathBuf::from("/tmp/foo.sln"),
131+
projects: vec![Project {
132+
path: PathBuf::from("/tmp/bar/bar.csproj"),
133+
target_framework: "net9.0".to_string(),
134+
project_type: ProjectType::ConsoleApplication,
135+
assembly_name: "bar".to_string(),
136+
}],
137+
};
138+
139+
let expected_processes = vec![Process {
140+
r#type: process_type!("bar"),
141+
command: vec![
142+
"bash".to_string(),
143+
"-c".to_string(),
144+
"cd bar/bin/publish; ./bar".to_string(),
145+
],
146+
args: vec![],
147+
default: false,
148+
working_directory: WorkingDirectory::App,
149+
}];
150+
151+
assert_eq!(
152+
detect_solution_processes(&solution).unwrap(),
153+
expected_processes
154+
);
155+
}
156+
157+
#[test]
158+
fn test_project_launch_process_non_executable() {
159+
let solution = Solution {
160+
path: PathBuf::from("/tmp/foo.sln"),
161+
projects: vec![Project {
162+
path: PathBuf::from("/tmp/bar/bar.csproj"),
163+
target_framework: "net9.0".to_string(),
164+
project_type: ProjectType::Unknown,
165+
assembly_name: "bar".to_string(),
166+
}],
167+
};
168+
169+
assert!(detect_solution_processes(&solution).unwrap().is_empty());
170+
}
171+
172+
#[test]
173+
fn test_project_executable_path() {
174+
let project = Project {
175+
path: PathBuf::from("/tmp/project/project.csproj"),
176+
target_framework: "net9.0".to_string(),
177+
project_type: ProjectType::ConsoleApplication,
178+
assembly_name: "TestApp".to_string(),
179+
};
180+
181+
assert_eq!(
182+
project_executable_path(&project),
183+
PathBuf::from("/tmp/project/bin/publish/TestApp")
184+
);
185+
}
186+
187+
#[test]
188+
fn test_relative_executable_path() {
189+
let solution = Solution {
190+
path: PathBuf::from("/tmp/solution.sln"),
191+
projects: vec![],
192+
};
193+
194+
let project = Project {
195+
path: PathBuf::from("/tmp/project/project.csproj"),
196+
target_framework: "net9.0".to_string(),
197+
project_type: ProjectType::ConsoleApplication,
198+
assembly_name: "TestApp".to_string(),
199+
};
200+
201+
assert_eq!(
202+
relative_executable_path(&solution, &project),
203+
PathBuf::from("project/bin/publish/TestApp")
204+
);
205+
}
67206
}

0 commit comments

Comments
 (0)
Please sign in to comment.