@sjcrh/proteinpaint-rust 2.193.0 → 2.196.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Cargo.toml CHANGED
@@ -75,10 +75,6 @@ path="src/sv.rs"
75
75
  name="cluster"
76
76
  path="src/cluster.rs"
77
77
 
78
- [[bin]]
79
- name="gdcmaf"
80
- path="src/gdcmaf.rs"
81
-
82
78
  [[bin]]
83
79
  name="wilcoxon"
84
80
  path="src/wilcoxon.rs"
@@ -95,22 +91,11 @@ path="src/genesetORA.rs"
95
91
  name="computeTopTerms"
96
92
  path="src/computeTopTerms.rs"
97
93
 
98
- [[bin]]
99
- name="readHDF5"
100
- path="src/readHDF5.rs"
101
-
102
- [[bin]]
103
- name="validateHDF5"
104
- path="src/validateHDF5.rs"
105
94
 
106
95
  [[bin]]
107
96
  name="cerno"
108
97
  path="src/cerno.rs"
109
98
 
110
- [[bin]]
111
- name="readH5"
112
- path="src/readH5.rs"
113
-
114
99
  [[bin]]
115
100
  name="dmrcate"
116
101
  path="src/dmrcate.rs"
package/index.js CHANGED
@@ -13,14 +13,11 @@ Standard output of the rust binary is returned.
13
13
  // Import necessary modules
14
14
  import fs from 'fs'
15
15
  import path from 'path'
16
- import { spawn, exec } from 'child_process'
17
- import { Readable, Transform } from 'stream'
18
- import { promisify } from 'util'
16
+ import { spawn } from 'child_process'
17
+ import { Readable } from 'stream'
19
18
 
20
19
  const __dirname = import.meta.dirname // set __dirname for consistency with cjs code
21
20
 
22
- const execPromise = promisify(exec)
23
-
24
21
  // Check if rust binary directory exists and is not empty
25
22
  const binaryDir = path.join(__dirname, '/target/release/')
26
23
  if (!fs.existsSync(binaryDir)) throw `missing rust binary directory='${binaryDir}'`
@@ -82,162 +79,3 @@ export function run_rust(binfile, input_data, args = [], { signal } = {}) {
82
79
  })
83
80
  })
84
81
  }
85
-
86
- // May need to add an abort signal as argument like in run_rust above.
87
- // Or it's likely not needed since a closed stream connection from a web browser
88
- // will already trigger an error. `stream_rust()` was heavily tested manually
89
- // while troubleshooting and fixing the idle rust processes the led to memory leaks
90
- // as part of `/gdc/mafBuild` handler code, leave this code as-is for now.
91
- export function stream_rust(binfile, input_data, emitJson) {
92
- const binpath = path.join(__dirname, '/target/release/', binfile)
93
-
94
- const ps = spawn(binpath)
95
- const childStream = new Transform({
96
- transform(chunk, encoding, callback) {
97
- this.push(chunk)
98
- callback()
99
- }
100
- })
101
- // we only want to run this interval loop inside a container, not in dev/test CI
102
- if (binfile == 'gdcmaf') trackByPid(ps.pid, binfile)
103
- const stderr = []
104
- try {
105
- // from route handler -> input_data -> ps.stdin -> ps.stdout -> transformed stream -> express response.pipe()
106
- Readable.from(input_data)
107
- .pipe(ps.stdin)
108
- .on('error', err => {
109
- emitErrors({ error: `error piping input data to spawned ${binfile} process` })
110
- })
111
- } catch (error) {
112
- console.log(`Error piping input_data into ${binfile}`, error)
113
- return
114
- }
115
-
116
- // uncomment to trigger childStream.destroy()
117
- // setTimeout(() => { console.log(74, 'childStream.destroy()'); childStream.destroy();}, 1000)
118
- // childStream.destroy() does not seem to trigger ps.stdout.pipe('...').on('error') callback,
119
- // which is okay as long as the server doesn't crash and ps get's killed eventually
120
- ps.stdout.pipe(childStream).on('error', err => console.log('ps.stdout.pipe(childStream) error', err))
121
-
122
- ps.stderr.on('data', data => stderr.push(data))
123
-
124
- ps.on('close', code => {
125
- if (trackedPids.has(ps.pid)) trackedPids.delete(ps.pid)
126
- if (stderr.length || killedPids.has(ps.pid) || code !== 0) {
127
- emitErrors(null, ps.pid, code)
128
- } else {
129
- emitJson()
130
- }
131
- })
132
- ps.on('error', err => {
133
- if (trackedPids.has(ps.pid)) trackedPids.delete(ps.pid)
134
- // console.log(74, `stream_rust().on('error')`, err)
135
- emitErrors(null, ps.pid)
136
- })
137
- ps.on('SIGTERM', err => {
138
- console.log(err)
139
- })
140
-
141
- function emitErrors(error, pid, code = 0) {
142
- // concatenate stderr uint8arr into a string
143
- let errors = stderr.join('').trim()
144
- if (error) errors += `\n` + error
145
- if (pid && killedPids.has(ps.pid) && !trackedPids.has(ps.pid)) {
146
- errors += '\n' + JSON.stringify({ error: `server error: MAF file processing terminated (expired process)` })
147
- killedPids.delete(pid)
148
- } else if (pid && code !== 0) {
149
- // may result from errors in spawned process code, or external signal (like `kill -9` in terminal)
150
- errors += '\n' + JSON.stringify({ error: `server error: MAF file processing terminated (code=${code})` })
151
- }
152
- emitJson(errors)
153
- }
154
-
155
- // on('end') will duplicate ps.on('close') event above
156
- // childStream.on('end', () => console.log(`childStream.on(end)`))
157
-
158
- // this may duplicate ps.on('error'), unless the error happened within the transform
159
- childStream.on('error', err => {
160
- console.log('stream_rust childStream.on(error)', err)
161
- try {
162
- childStream.destroy(err)
163
- } catch (e) {
164
- console.log(e)
165
- }
166
- })
167
-
168
- function endStream() {
169
- try {
170
- if (!childStream.writableEnded) {
171
- console.log('trigger childStream.destroy() in endStream()')
172
- childStream.destroy()
173
- }
174
- } catch (e) {
175
- console.log('error triggering childStream.destroy()', e)
176
- }
177
- try {
178
- if (!ps.killed) {
179
- console.log('trigger ps.kill() in endStream()')
180
- ps.kill()
181
- }
182
- if (trackedPids.has(ps.pid)) trackedPids.delete(ps.pid)
183
- } catch (e) {
184
- console.log('error triggering ps.kill()', e)
185
- }
186
- }
187
-
188
- return { rustStream: childStream, endStream }
189
- }
190
-
191
- const trackedPids = new Map() // will be used to monitor expired processes
192
- const killedPids = new Set() // will be used to detect killed processes, to help with error detection
193
- const PSKILL_INTERVAL_MS = 30000 // every 30 seconds
194
- let psKillInterval
195
-
196
- // default maxElapsed = 5 * 60 * 1000 millisecond = 300000 or 5 minutes, change to 0 to test
197
- // may allow configuration of maxElapsed by dataset/argument
198
- function trackByPid(pid, name, maxElapsed = 300000) {
199
- if (!pid) return
200
- // only track by value (integer, string), not reference object
201
- // NOTE: a reused/reassigned process.pid will be replaced by the most recent process
202
- trackedPids.set(pid, { name, expires: Date.now() + maxElapsed })
203
- if (!psKillInterval) psKillInterval = setInterval(killExpiredProcesses, PSKILL_INTERVAL_MS)
204
- // uncomment below to test
205
- // console.log([...trackedPids.entries()])
206
- // if (maxElapsed < 10000) setTimeout(killExpiredProcesses, 1000) // uncomment for testing only
207
- }
208
-
209
- //
210
- // Use one setInterval() to monitor >= 1 process,
211
- // instead of a separate setTimeout() for each process.
212
- // This is more reliable as setTimeout would use spawned ps.kill(),
213
- // which may not exist when the timeout callback is executed and
214
- // thus would require clearTimeout(closured_variable). Tracking by
215
- // pid does not rely on a usable 'ps' variable to kill itself.
216
- //
217
- function killExpiredProcesses() {
218
- //console.log(149, 'killExpiredProcesses()')
219
- killedPids.clear()
220
- const time = Date.now()
221
- for (const [pid, info] of trackedPids.entries()) {
222
- if (info.expires > time) continue
223
- try {
224
- // true if process exists
225
- process.kill(pid, 0)
226
- } catch (_) {
227
- // no need to kill, but remove from tracking
228
- trackedPids.delete(pid)
229
- // prevent misleading logs of 'unable to kill ...'
230
- continue
231
- }
232
- const label = `rust process ${info.name} (pid=${pid})`
233
- try {
234
- // detect if process exists before killing it
235
- process.kill(pid, 'SIGTERM')
236
- trackedPids.delete(pid)
237
- killedPids.add(pid)
238
- console.log(`killed ${label}`)
239
- } catch (err) {
240
- console.log(`unable to kill ${label}`, err)
241
- }
242
- }
243
- }
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.193.0",
2
+ "version": "2.196.0",
3
3
  "name": "@sjcrh/proteinpaint-rust",
4
4
  "type": "module",
5
5
  "description": "Rust-based utilities for proteinpaint",
package/src/gdcmaf.rs DELETED
@@ -1,318 +0,0 @@
1
- /*
2
- This script download cohort maf files from GDC, concatenate them into a single file that includes user specified columns.
3
-
4
- Input JSON:
5
- host: GDC host
6
- fileIdLst: An array of uuid
7
- headers: required headers for GDC API
8
- Output gzip compressed maf file to stdout.
9
-
10
- Example of usage:
11
- headers='{"X-Forwarded-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.…ML, like Gecko) Chrome/142.0.0.0 Safari/537.36 Edg/142.0.0.0", "X-Forwarded-For": "127.0.0.1"}'
12
- echo '{"host": "https://api.gdc.cancer.gov/data/","columns": ["Hugo_Symbol", "Entrez_Gene_Id", "Center", "NCBI_Build", "Chromosome", "Start_Position"], "fileIdLst": ["8b31d6d1-56f7-4aa8-b026-c64bafd531e7", "b429fcc1-2b59-4b4c-a472-fb27758f6249"], "headers": '$headers'}'|./target/release/gdcmaf
13
- */
14
-
15
- use flate2::Compression;
16
- use flate2::read::GzDecoder;
17
- use flate2::write::GzEncoder;
18
- use futures::StreamExt;
19
- use serde_json::Value;
20
- use std::io::{self, Read, Write};
21
- use std::sync::{Arc, Mutex};
22
- use std::time::Duration;
23
- use tokio::io::{AsyncReadExt, BufReader};
24
- use tokio::time::timeout;
25
-
26
- // Struct to hold error information
27
- #[derive(serde::Serialize)]
28
- struct ErrorEntry {
29
- url: String,
30
- error: String,
31
- }
32
-
33
- fn select_maf_col(d: String, columns: &Vec<String>, url: &str) -> Result<(Vec<u8>, i32), (String, String)> {
34
- let mut maf_str: String = String::new();
35
- let mut header_indices: Vec<usize> = Vec::new();
36
- let lines = d.trim_end().split("\n");
37
- let mut mafrows = 0;
38
- for line in lines {
39
- if line.starts_with("#") {
40
- continue;
41
- } else if line.contains("Hugo_Symbol") {
42
- let header: Vec<String> = line.split("\t").map(|s| s.to_string()).collect();
43
- for col in columns {
44
- match header.iter().position(|x| x == col) {
45
- Some(index) => {
46
- header_indices.push(index);
47
- }
48
- None => {
49
- let error_msg = format!("Column {} was not found", col);
50
- return Err((url.to_string(), error_msg));
51
- }
52
- }
53
- }
54
- if header_indices.is_empty() {
55
- return Err((url.to_string(), "No matching columns found".to_string()));
56
- }
57
- } else {
58
- let maf_cont_lst: Vec<String> = line.split("\t").map(|s| s.to_string()).collect();
59
- let mut maf_out_lst: Vec<String> = Vec::new();
60
- for x in header_indices.iter() {
61
- maf_out_lst.push(maf_cont_lst[*x].to_string());
62
- }
63
- maf_str.push_str(maf_out_lst.join("\t").as_str());
64
- maf_str.push_str("\n");
65
- mafrows += 1;
66
- }
67
- }
68
- Ok((maf_str.as_bytes().to_vec(), mafrows))
69
- }
70
-
71
- #[tokio::main]
72
- async fn main() -> Result<(), Box<dyn std::error::Error>> {
73
- // Accepting the piped input json from jodejs and assign to the variable
74
- // host: GDC host
75
- // url: urls to download single maf files
76
- let timeout_duration = Duration::from_secs(5); // Set a 10-second timeout
77
-
78
- // Wrap the read operation in a timeout
79
- let result = timeout(timeout_duration, async {
80
- let mut buffer = String::new(); // Initialize an empty string to store input
81
- let mut reader = BufReader::new(tokio::io::stdin()); // Create a buffered reader for stdin
82
- reader.read_to_string(&mut buffer).await?; // Read a line asynchronously
83
- Ok::<String, io::Error>(buffer) // Return the input as a Result
84
- })
85
- .await;
86
- // Handle the result of the timeout operation
87
- let file_id_lst_js: Value = match result {
88
- Ok(Ok(buffer)) => match serde_json::from_str(&buffer) {
89
- Ok(js) => js,
90
- Err(e) => {
91
- let stdin_error = ErrorEntry {
92
- url: String::new(),
93
- error: format!("JSON parsing error: {}", e),
94
- };
95
- writeln!(io::stderr(), "{}", serde_json::to_string(&stdin_error).unwrap()).unwrap();
96
- return Err(Box::new(std::io::Error::new(
97
- std::io::ErrorKind::InvalidInput,
98
- "JSON parsing error!",
99
- )) as Box<dyn std::error::Error>);
100
- }
101
- },
102
- Ok(Err(_e)) => {
103
- let stdin_error = ErrorEntry {
104
- url: String::new(),
105
- error: "Error reading from stdin.".to_string(),
106
- };
107
- let stdin_error_js = serde_json::to_string(&stdin_error).unwrap();
108
- writeln!(io::stderr(), "{}", stdin_error_js).expect("Failed to output stderr!");
109
- return Err(Box::new(std::io::Error::new(
110
- std::io::ErrorKind::InvalidInput,
111
- "Failed to output stderr!",
112
- )) as Box<dyn std::error::Error>);
113
- }
114
- Err(_) => {
115
- let stdin_error = ErrorEntry {
116
- url: String::new(),
117
- error: "Timeout while reading from stdin.".to_string(),
118
- };
119
- let stdin_error_js = serde_json::to_string(&stdin_error).unwrap();
120
- writeln!(io::stderr(), "{}", stdin_error_js).expect("Failed to output stderr!");
121
- return Err(Box::new(std::io::Error::new(
122
- std::io::ErrorKind::InvalidInput,
123
- "The columns in arg is not an array",
124
- )) as Box<dyn std::error::Error>);
125
- }
126
- };
127
-
128
- // reading the input from PP
129
- let host = file_id_lst_js
130
- .get("host")
131
- .expect("Host was not provided")
132
- .as_str()
133
- .expect("Host is not a string");
134
- let mut url: Vec<String> = Vec::new();
135
- let file_id_lst = file_id_lst_js
136
- .get("fileIdLst")
137
- .expect("File ID list is missed!")
138
- .as_array()
139
- .expect("File ID list is not an array");
140
- for v in file_id_lst {
141
- //url.push(Path::new(&host).join(&v.as_str().unwrap()).display().to_string());
142
- url.push(format!("{}/{}", host.trim_end_matches('/'), v.as_str().unwrap()));
143
- }
144
- let mut req_headers = reqwest::header::HeaderMap::new();
145
- if let Some(headers_val) = file_id_lst_js.get("headers") {
146
- let headers_obj = match headers_val.as_object() {
147
- Some(obj) => obj,
148
- None => {
149
- let header_error = ErrorEntry {
150
- url: String::new(),
151
- error: "headers is not an object".to_string(),
152
- };
153
- let header_error_js = serde_json::to_string(&header_error).unwrap();
154
- writeln!(io::stderr(), "{}", header_error_js).expect("Failed to output stderr!");
155
- return Err(Box::new(std::io::Error::new(
156
- std::io::ErrorKind::InvalidInput,
157
- "headers is not an object",
158
- )) as Box<dyn std::error::Error>);
159
- }
160
- };
161
- for (key, value) in headers_obj {
162
- req_headers.insert(
163
- reqwest::header::HeaderName::from_bytes(key.as_bytes()).expect("Invalid header key"),
164
- reqwest::header::HeaderValue::from_str(value.as_str().expect("Invalid string value"))
165
- .expect("Invalid header value"),
166
- );
167
- }
168
- }
169
-
170
- // read columns as array from input json and convert data type from Vec<Value> to Vec<String>
171
- let maf_col: Vec<String>;
172
- if let Some(maf_col_value) = file_id_lst_js.get("columns") {
173
- //convert Vec<Value> to Vec<String>
174
- if let Some(maf_col_array) = maf_col_value.as_array() {
175
- maf_col = maf_col_array
176
- .iter()
177
- .map(|v| v.to_string().replace("\"", ""))
178
- .collect::<Vec<String>>();
179
- } else {
180
- let column_error = ErrorEntry {
181
- url: String::new(),
182
- error: "The columns in arg is not an array".to_string(),
183
- };
184
- let column_error_js = serde_json::to_string(&column_error).unwrap();
185
- writeln!(io::stderr(), "{}", column_error_js).expect("Failed to output stderr!");
186
- return Err(Box::new(std::io::Error::new(
187
- std::io::ErrorKind::InvalidInput,
188
- "The columns in arg is not an array",
189
- )) as Box<dyn std::error::Error>);
190
- }
191
- } else {
192
- let column_error = ErrorEntry {
193
- url: String::new(),
194
- error: "Columns was not selected".to_string(),
195
- };
196
- let column_error_js = serde_json::to_string(&column_error).unwrap();
197
- writeln!(io::stderr(), "{}", column_error_js).expect("Failed to output stderr!");
198
- return Err(Box::new(std::io::Error::new(
199
- std::io::ErrorKind::InvalidInput,
200
- "Columns was not selected",
201
- )) as Box<dyn std::error::Error>);
202
- };
203
-
204
- //downloading maf files parallelly and merge them into single maf file
205
- let download_futures = futures::stream::iter(url.into_iter().map(|url| {
206
- let req_headers_clone = req_headers.clone();
207
- async move {
208
- let client = reqwest::Client::builder()
209
- .timeout(Duration::from_secs(60)) // 60-second timeout per request
210
- .connect_timeout(Duration::from_secs(15))
211
- .default_headers(req_headers_clone.clone())
212
- .build()
213
- .map_err(|_e| {
214
- let client_error = ErrorEntry {
215
- url: url.clone(),
216
- error: "Client build error".to_string(),
217
- };
218
- let client_error_js = serde_json::to_string(&client_error).unwrap();
219
- writeln!(io::stderr(), "{}", client_error_js).expect("Failed to build reqwest client!");
220
- });
221
- match client.unwrap().get(&url).send().await {
222
- Ok(resp) if resp.status().is_success() => match resp.bytes().await {
223
- Ok(content) => {
224
- let mut decoder = GzDecoder::new(&content[..]);
225
- let mut decompressed_content = Vec::new();
226
- match decoder.read_to_end(&mut decompressed_content) {
227
- Ok(_) => {
228
- let text = String::from_utf8_lossy(&decompressed_content).to_string();
229
- return Ok((url.clone(), text));
230
- }
231
- Err(e) => {
232
- let error_msg = format!("Failed to decompress downloaded MAF file: {}", e);
233
- Err((url.clone(), error_msg))
234
- }
235
- }
236
- }
237
- Err(e) => {
238
- let error_msg = format!("Failed to decompress downloaded MAF file: {}", e);
239
- Err((url.clone(), error_msg))
240
- }
241
- },
242
- Ok(resp) => {
243
- let error_msg = format!("HTTP error: {}", resp.status());
244
- Err((url.clone(), error_msg))
245
- }
246
- Err(e) => {
247
- let error_msg = format!("Server request failed: {}", e);
248
- Err((url.clone(), error_msg))
249
- }
250
- }
251
- }
252
- }));
253
-
254
- // binary output
255
- let encoder = Arc::new(Mutex::new(GzEncoder::new(io::stdout(), Compression::default())));
256
-
257
- // Write the header
258
- {
259
- let mut encoder_guard = encoder.lock().unwrap(); // Lock the Mutex to get access to the inner GzEncoder
260
- encoder_guard
261
- .write_all(&maf_col.join("\t").as_bytes().to_vec())
262
- .expect("Failed to write header");
263
- encoder_guard.write_all(b"\n").expect("Failed to write newline");
264
- }
265
-
266
- download_futures
267
- .buffer_unordered(20)
268
- .for_each(|result| {
269
- let encoder = Arc::clone(&encoder); // Clone the Arc for each task
270
- let maf_col_cp = maf_col.clone();
271
- async move {
272
- match result {
273
- Ok((url, content)) => match select_maf_col(content, &maf_col_cp, &url) {
274
- Ok((maf_bit, mafrows)) => {
275
- if mafrows > 0 {
276
- let mut encoder_guard = encoder.lock().unwrap();
277
- encoder_guard.write_all(&maf_bit).expect("Failed to write file");
278
- } else {
279
- let error = ErrorEntry {
280
- url: url.clone(),
281
- error: "Empty MAF file".to_string(),
282
- };
283
- let error_js = serde_json::to_string(&error).unwrap();
284
- writeln!(io::stderr(), "{}", error_js).expect("Failed to output stderr!");
285
- }
286
- }
287
- Err((url, error)) => {
288
- let error = ErrorEntry { url, error };
289
- let error_js = serde_json::to_string(&error).unwrap();
290
- writeln!(io::stderr(), "{}", error_js).expect("Failed to output stderr!");
291
- }
292
- },
293
- Err((url, error)) => {
294
- let error = ErrorEntry { url, error };
295
- let error_js = serde_json::to_string(&error).unwrap();
296
- writeln!(io::stderr(), "{}", error_js).expect("Failed to output stderr!");
297
- }
298
- };
299
- }
300
- })
301
- .await;
302
-
303
- // Finalize output
304
-
305
- // Replace the value inside the Mutex with a dummy value (e.g., None)
306
- let mut encoder_guard = encoder.lock().unwrap();
307
- let encoder = std::mem::replace(
308
- &mut *encoder_guard,
309
- GzEncoder::new(io::stdout(), Compression::default()),
310
- );
311
- // Finalize the encoder
312
- encoder.finish().expect("Maf file output error!");
313
-
314
- // Manually flush stdout and stderr
315
- io::stdout().flush().expect("Failed to flush stdout");
316
- io::stderr().flush().expect("Failed to flush stderr");
317
- Ok(())
318
- }