functiontrace 0.4.4__tar.gz → 0.5.0__tar.gz
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.
- {functiontrace-0.4.4 → functiontrace-0.5.0}/Cargo.toml +7 -1
- {functiontrace-0.4.4/functiontrace.egg-info → functiontrace-0.5.0}/PKG-INFO +1 -1
- {functiontrace-0.4.4 → functiontrace-0.5.0/functiontrace.egg-info}/PKG-INFO +1 -1
- {functiontrace-0.4.4 → functiontrace-0.5.0}/functiontrace.py +3 -14
- {functiontrace-0.4.4 → functiontrace-0.5.0}/pyproject.toml +1 -1
- {functiontrace-0.4.4 → functiontrace-0.5.0}/rust/lib.rs +515 -20
- {functiontrace-0.4.4 → functiontrace-0.5.0}/LICENSE.md +0 -0
- {functiontrace-0.4.4 → functiontrace-0.5.0}/MANIFEST.in +0 -0
- {functiontrace-0.4.4 → functiontrace-0.5.0}/README.rst +0 -0
- {functiontrace-0.4.4 → functiontrace-0.5.0}/build.rs +0 -0
- {functiontrace-0.4.4 → functiontrace-0.5.0}/functiontrace.egg-info/SOURCES.txt +0 -0
- {functiontrace-0.4.4 → functiontrace-0.5.0}/functiontrace.egg-info/dependency_links.txt +0 -0
- {functiontrace-0.4.4 → functiontrace-0.5.0}/functiontrace.egg-info/entry_points.txt +0 -0
- {functiontrace-0.4.4 → functiontrace-0.5.0}/functiontrace.egg-info/top_level.txt +0 -0
- {functiontrace-0.4.4 → functiontrace-0.5.0}/setup.cfg +0 -0
- {functiontrace-0.4.4 → functiontrace-0.5.0}/setup.py +0 -0
|
@@ -7,13 +7,19 @@ edition = "2021"
|
|
|
7
7
|
color-eyre = { version = "0.6", default-features = false }
|
|
8
8
|
dashmap = "6"
|
|
9
9
|
functiontrace-server = { version = "0.8.4", default-features = false, features = ["serialize"] }
|
|
10
|
+
jiff = { version = "0.2", default-features = false, features = ["std", "serde"] }
|
|
10
11
|
libc = "0.2"
|
|
12
|
+
owo-colors = "4.2"
|
|
11
13
|
pyo3 = { version = "0.25", features = ["eyre"] }
|
|
14
|
+
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] }
|
|
12
15
|
rmp-serde = "1.3"
|
|
13
16
|
serde = "1"
|
|
17
|
+
toml = "0.9"
|
|
18
|
+
tracing = { version = "0.1", features = ["log"] }
|
|
19
|
+
tracing-subscriber = { version = "0.3", default-features = false, features = ["ansi", "env-filter", "fmt", "std"] }
|
|
14
20
|
|
|
15
21
|
[build-dependencies]
|
|
16
|
-
toml = { version = "0.
|
|
22
|
+
toml = { version = "0.9", default-features = false, features = ["parse", "serde"] }
|
|
17
23
|
pyo3-build-config = { version = "0.25", features = ["resolve-config"] }
|
|
18
24
|
|
|
19
25
|
[lib]
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import argparse
|
|
2
2
|
import os
|
|
3
|
-
import shutil
|
|
4
3
|
import sys
|
|
5
4
|
import tempfile
|
|
6
5
|
from importlib import machinery, util
|
|
@@ -20,19 +19,6 @@ exec $(which {python}) -m functiontrace "$@"
|
|
|
20
19
|
|
|
21
20
|
|
|
22
21
|
def setup_dependencies():
|
|
23
|
-
# We need the functiontrace-server installed and locatable in order to
|
|
24
|
-
# trace anything.
|
|
25
|
-
if shutil.which("functiontrace-server") is None:
|
|
26
|
-
print(
|
|
27
|
-
"Unable to find `functiontrace-server` in the current $PATH.",
|
|
28
|
-
file=sys.stderr,
|
|
29
|
-
)
|
|
30
|
-
print(
|
|
31
|
-
"See https://functiontrace.com#installation for installation instructions.",
|
|
32
|
-
file=sys.stderr,
|
|
33
|
-
)
|
|
34
|
-
sys.exit(1)
|
|
35
|
-
|
|
36
22
|
# Generate a temp directory to store our wrappers in. We'll temporarily
|
|
37
23
|
# add this directory to our path.
|
|
38
24
|
tempdir = tempfile.mkdtemp(prefix="py-functiontrace")
|
|
@@ -79,6 +65,9 @@ def main():
|
|
|
79
65
|
parser.print_help()
|
|
80
66
|
sys.exit(1)
|
|
81
67
|
|
|
68
|
+
# Load our config and verify the license before we trace.
|
|
69
|
+
rust.load_config()
|
|
70
|
+
|
|
82
71
|
# Ignore ourselves, keeping sys.argv looking reasonable as the child script
|
|
83
72
|
# will expect it to be sane.
|
|
84
73
|
sys.argv[:] = args.script
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
#[pyo3::pymodule]
|
|
2
2
|
mod _functiontrace_rs {
|
|
3
|
-
use color_eyre::eyre::{
|
|
3
|
+
use color_eyre::eyre::{OptionExt, Result, WrapErr, eyre};
|
|
4
4
|
use functiontrace_server::function_trace;
|
|
5
|
-
use
|
|
6
|
-
use
|
|
5
|
+
use jiff::Timestamp;
|
|
6
|
+
use pyo3::prelude::*;
|
|
7
|
+
use serde::{Deserialize, Serialize};
|
|
7
8
|
use std::borrow::Cow;
|
|
8
9
|
use std::cell::Cell;
|
|
9
|
-
use std::ffi::{
|
|
10
|
+
use std::ffi::{CStr, OsString, c_void};
|
|
10
11
|
use std::io::Write;
|
|
11
12
|
use std::os::unix::net::UnixStream;
|
|
12
13
|
use std::sync::atomic::{AtomicBool, Ordering};
|
|
@@ -16,6 +17,18 @@ mod _functiontrace_rs {
|
|
|
16
17
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
17
18
|
// Core Types
|
|
18
19
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
20
|
+
|
|
21
|
+
/// The configuration file for FunctionTrace.
|
|
22
|
+
///
|
|
23
|
+
/// TODO: This is currently only used for licensing, but could reasonably be used to augment or
|
|
24
|
+
/// replace some of the env vars we use.
|
|
25
|
+
#[derive(Serialize, Deserialize, Clone, PartialEq)]
|
|
26
|
+
struct Config {
|
|
27
|
+
/// The user's license key. This is NONCOMMERCIAL where relevant, and otherwise must
|
|
28
|
+
/// contain a key that will be validated on startup.
|
|
29
|
+
license_key: Option<String>,
|
|
30
|
+
}
|
|
31
|
+
|
|
19
32
|
/// The size of messagepack buffers. This is picked somewhat arbitrarily, but should be
|
|
20
33
|
/// reasonably small since we'll need one per thread, and should be large enough that we
|
|
21
34
|
/// can fit many messages in before needing to do expensive UnixStream syscalls.
|
|
@@ -72,7 +85,7 @@ mod _functiontrace_rs {
|
|
|
72
85
|
|
|
73
86
|
/// Flush our local buffer to the socket.
|
|
74
87
|
fn flush(&mut self) -> std::io::Result<()> {
|
|
75
|
-
// We're about to send all
|
|
88
|
+
// We're about to send all the data, so reset our buffer. Do this now to ensure we
|
|
76
89
|
// don't miss it, since the buffer will be left in an invalid state if this doesn't
|
|
77
90
|
// happen.
|
|
78
91
|
let head = self.head;
|
|
@@ -188,6 +201,24 @@ mod _functiontrace_rs {
|
|
|
188
201
|
|
|
189
202
|
color_eyre::install().wrap_err("Failed to install color_eyre")?;
|
|
190
203
|
|
|
204
|
+
// If someone else has already installed a tracing subscriber, we'll fail to initialize
|
|
205
|
+
// but get _something_ anyways.
|
|
206
|
+
// This may not be possible, but I suspect a different Rust extension could interfere with
|
|
207
|
+
// us so might as well be defensive.
|
|
208
|
+
if let Err(e) = tracing_subscriber::fmt::fmt()
|
|
209
|
+
.with_target(false)
|
|
210
|
+
.with_ansi(true)
|
|
211
|
+
.with_env_filter(tracing_subscriber::filter::EnvFilter::from_default_env())
|
|
212
|
+
.try_init()
|
|
213
|
+
{
|
|
214
|
+
tracing::error!(
|
|
215
|
+
error = e,
|
|
216
|
+
"Failed to install tracing_subscriber for FunctionTrace"
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
tracing::info!("Loaded `functiontrace` python module!");
|
|
221
|
+
|
|
191
222
|
Ok(())
|
|
192
223
|
}
|
|
193
224
|
|
|
@@ -228,11 +259,24 @@ mod _functiontrace_rs {
|
|
|
228
259
|
// Launch the functiontrace-server as a daemon, then record the address of the socket we'll
|
|
229
260
|
// need to communicate with it on.
|
|
230
261
|
let sockaddr = {
|
|
231
|
-
let server = std::process::Command::new("functiontrace-server")
|
|
262
|
+
let server = match std::process::Command::new("functiontrace-server")
|
|
232
263
|
.arg("--directory")
|
|
233
264
|
.arg(output_directory)
|
|
234
265
|
.spawn()
|
|
235
|
-
|
|
266
|
+
{
|
|
267
|
+
Ok(server) => server,
|
|
268
|
+
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
|
269
|
+
eprintln!("Unable to find `functiontrace-server` in the current $PATH.");
|
|
270
|
+
eprintln!(
|
|
271
|
+
"See https://functiontrace.com#installation for installation instructions."
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
// Exit here rather than returning an error, since that'll emit a confusing
|
|
275
|
+
// Python stacktrace.
|
|
276
|
+
std::process::exit(1);
|
|
277
|
+
}
|
|
278
|
+
e => e.wrap_err("Failed to launch functiontrace-server")?,
|
|
279
|
+
};
|
|
236
280
|
|
|
237
281
|
format!("/tmp/functiontrace-server.sock.{}", server.id()).into()
|
|
238
282
|
};
|
|
@@ -299,7 +343,8 @@ mod _functiontrace_rs {
|
|
|
299
343
|
Ok(full_args)
|
|
300
344
|
})
|
|
301
345
|
.unwrap_or_else(|_err| {
|
|
302
|
-
|
|
346
|
+
tracing::warn!("Failed to read ARGV");
|
|
347
|
+
|
|
303
348
|
"<UNKNOWN>".into()
|
|
304
349
|
});
|
|
305
350
|
|
|
@@ -333,7 +378,8 @@ mod _functiontrace_rs {
|
|
|
333
378
|
.into())
|
|
334
379
|
})
|
|
335
380
|
.unwrap_or_else(|_err| {
|
|
336
|
-
|
|
381
|
+
tracing::warn!("Failed to read sys.platform");
|
|
382
|
+
|
|
337
383
|
"<UNKNOWN>".into()
|
|
338
384
|
});
|
|
339
385
|
|
|
@@ -431,7 +477,7 @@ mod _functiontrace_rs {
|
|
|
431
477
|
}
|
|
432
478
|
|
|
433
479
|
/// Called when the process is shutting down via `atexit`, notifies us to stop tracing and
|
|
434
|
-
/// flush the buffer.
|
|
480
|
+
/// flush the buffer.
|
|
435
481
|
///
|
|
436
482
|
/// NOTE: This is always called on the main thread.
|
|
437
483
|
fn trace_terminate(
|
|
@@ -596,7 +642,7 @@ mod _functiontrace_rs {
|
|
|
596
642
|
}
|
|
597
643
|
|
|
598
644
|
/// Enable memory tracing for the current program. This is implemented by attaching wrapper
|
|
599
|
-
/// versions of malloc/free/etc to all
|
|
645
|
+
/// versions of malloc/free/etc to all the current allocators.
|
|
600
646
|
///
|
|
601
647
|
/// NOTE: Memory tracing may have up to 40% overhead on traces with many small allocations, so
|
|
602
648
|
/// is not enabled by default.
|
|
@@ -825,7 +871,12 @@ mod _functiontrace_rs {
|
|
|
825
871
|
if let Ok(module) = m_module.downcast::<pyo3::types::PyString>() {
|
|
826
872
|
module.to_cow().unwrap_or(Cow::Borrowed("<invalid string>"))
|
|
827
873
|
} else {
|
|
828
|
-
|
|
874
|
+
if let Ok(typ) = m_module.get_type().qualname().map(|x| x.to_string()) {
|
|
875
|
+
tracing::warn!(type = typ, "Failed to read module name due to unexpected type");
|
|
876
|
+
} else {
|
|
877
|
+
tracing::warn!("Failed to read module name due to indecipherable type");
|
|
878
|
+
}
|
|
879
|
+
|
|
829
880
|
Cow::Borrowed("<unknown module>")
|
|
830
881
|
};
|
|
831
882
|
function_trace::FunctionTrace::NativeCall {
|
|
@@ -881,13 +932,452 @@ mod _functiontrace_rs {
|
|
|
881
932
|
// We unfortunately can't get these from `PyEval_SetProfile`, so we'd need to use a
|
|
882
933
|
// different method to capture them.
|
|
883
934
|
}
|
|
884
|
-
|
|
885
|
-
|
|
935
|
+
v => {
|
|
936
|
+
tracing::error!(event = v, "Unexpected PyTrace message!");
|
|
886
937
|
}
|
|
887
938
|
};
|
|
888
939
|
|
|
889
940
|
0
|
|
890
941
|
}
|
|
942
|
+
|
|
943
|
+
////////////////////////////////////////////////////////////////////////////
|
|
944
|
+
// Commercial support
|
|
945
|
+
////////////////////////////////////////////////////////////////////////////
|
|
946
|
+
|
|
947
|
+
/// Load our configuration, including:
|
|
948
|
+
/// - Displaying a first-use message
|
|
949
|
+
/// - Creating the configuration file if non-existent
|
|
950
|
+
/// - Validating license keys
|
|
951
|
+
///
|
|
952
|
+
/// We default to being permissive, and want to make this as non-invasive as possible for
|
|
953
|
+
/// non-commercial users.
|
|
954
|
+
#[pyfunction]
|
|
955
|
+
fn load_config() -> Result<()> {
|
|
956
|
+
use owo_colors::OwoColorize;
|
|
957
|
+
|
|
958
|
+
////////////////////////////////////////////////////////////////////////
|
|
959
|
+
// Load or initialize config file
|
|
960
|
+
////////////////////////////////////////////////////////////////////////
|
|
961
|
+
// Load configuration from the standard directory, or `~/.config` if the XDG spec isn't
|
|
962
|
+
// being followed.
|
|
963
|
+
let config_path = {
|
|
964
|
+
if let Some(config_path) = std::env::var_os("FUNCTIONTRACE_CONFIGFILE") {
|
|
965
|
+
// Allow `FUNCTIONTRACE_CONFIGFILE` as an override (mostly for tests, but it could
|
|
966
|
+
// be useful for Nix users or others).
|
|
967
|
+
std::path::PathBuf::from(config_path)
|
|
968
|
+
} else if let Some(mut config_dir) = std::env::var_os("XDG_CONFIG_HOME")
|
|
969
|
+
.map(std::path::PathBuf::from)
|
|
970
|
+
.or_else(|| {
|
|
971
|
+
std::env::home_dir().map(|mut home| {
|
|
972
|
+
home.push(".config");
|
|
973
|
+
home
|
|
974
|
+
})
|
|
975
|
+
})
|
|
976
|
+
{
|
|
977
|
+
config_dir.push("functiontrace.toml");
|
|
978
|
+
config_dir
|
|
979
|
+
} else {
|
|
980
|
+
// If we fail to find a reasonable config path, we'll just pretend everything is
|
|
981
|
+
// fine.
|
|
982
|
+
tracing::error!(
|
|
983
|
+
"Failed to find FunctionTrace config directory - proceeding without loading config"
|
|
984
|
+
);
|
|
985
|
+
return Ok(());
|
|
986
|
+
}
|
|
987
|
+
};
|
|
988
|
+
|
|
989
|
+
let config = if let Ok(config) = std::fs::read(&config_path)
|
|
990
|
+
.wrap_err("Failed to read config file")
|
|
991
|
+
.and_then(|x| toml::from_slice::<Config>(&x).wrap_err("Failed to parse config file"))
|
|
992
|
+
{
|
|
993
|
+
// We successfully loaded an existing config file.
|
|
994
|
+
config
|
|
995
|
+
} else {
|
|
996
|
+
// There's no config file, so assume this is the first time FunctionTrace has been
|
|
997
|
+
// used and generate a new one.
|
|
998
|
+
let logo = "\
|
|
999
|
+
███████╗██╗ ██╗███╗ ██╗ ██████╗████████╗██╗ ██████╗ ███╗ ██╗████████╗██████╗ █████╗ ██████╗███████╗\n\
|
|
1000
|
+
██╔════╝██║ ██║████╗ ██║██╔════╝╚══██╔══╝██║██╔═══██╗████╗ ██║╚══██╔══╝██╔══██╗██╔══██╗██╔════╝██╔════╝\n\
|
|
1001
|
+
█████╗ ██║ ██║██╔██╗ ██║██║ ██║ ██║██║ ██║██╔██╗ ██║ ██║ ██████╔╝███████║██║ █████╗ \n\
|
|
1002
|
+
██╔══╝ ██║ ██║██║╚██╗██║██║ ██║ ██║██║ ██║██║╚██╗██║ ██║ ██╔══██╗██╔══██║██║ ██╔══╝ \n\
|
|
1003
|
+
██║ ╚██████╔╝██║ ╚████║╚██████╗ ██║ ██║╚██████╔╝██║ ╚████║ ██║ ██║ ██║██║ ██║╚██████╗███████╗\n\
|
|
1004
|
+
╚═╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝╚══════╝\n";
|
|
1005
|
+
|
|
1006
|
+
// Render the logo like a flamechart using Python-esque colors.
|
|
1007
|
+
for (row, line) in logo.lines().enumerate() {
|
|
1008
|
+
for (x, c) in line.chars().enumerate() {
|
|
1009
|
+
// heights[x] represents the height of the flamechart at position x
|
|
1010
|
+
//
|
|
1011
|
+
// NOTE: These values were somewhat arbitrarily chosen, but the bottom row is
|
|
1012
|
+
// always covered.
|
|
1013
|
+
let heights = [
|
|
1014
|
+
1, 1, 3, 3, 4, 4, 4, 4, 3, 2, 2, 2, 2, 2, 3, 4, 5, 5, 3, 3, 1, 2, 2, 3, 4,
|
|
1015
|
+
4, 4, 4, 4, 4, 4, 3, 3, 3, 5, 5, 6, 6, 6, 5, 5, 3, 2, 2, 2, 3, 5, 5, 5, 5,
|
|
1016
|
+
5, 2, 2, 4, 6, 6, 6, 3, 2, 3, 3, 5, 3, 3, 5, 5, 5, 5, 3, 3, 2, 2, 1, 1, 1,
|
|
1017
|
+
1, 1, 2, 2, 2, 4, 4, 4, 3, 4, 3, 3, 3, 3, 2, 2, 5, 5, 5, 5, 5, 5, 4, 3, 2,
|
|
1018
|
+
2, 2, 2, 3, 4, 5, 5, 0,
|
|
1019
|
+
];
|
|
1020
|
+
|
|
1021
|
+
// We look at chart depth from top to bottom.
|
|
1022
|
+
let current_depth = 6 - row;
|
|
1023
|
+
|
|
1024
|
+
if current_depth > heights[x] {
|
|
1025
|
+
print!("{}", c.blue());
|
|
1026
|
+
} else {
|
|
1027
|
+
print!("{}", c.fg::<owo_colors::colors::xterm::FlushOrange>());
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
println!();
|
|
1031
|
+
}
|
|
1032
|
+
println!();
|
|
1033
|
+
println!(
|
|
1034
|
+
"{}",
|
|
1035
|
+
"Welcome to FunctionTrace, a low overheard tracing profiler for Python".bold()
|
|
1036
|
+
);
|
|
1037
|
+
println!();
|
|
1038
|
+
println!(
|
|
1039
|
+
"Please see {} for instructions on setup and use.",
|
|
1040
|
+
"https://functiontrace.com".underline().cyan()
|
|
1041
|
+
);
|
|
1042
|
+
println!(
|
|
1043
|
+
"FunctionTrace will emit profile files ({}) that must be loaded by the",
|
|
1044
|
+
"functiontrace.*.tar.gz".dimmed()
|
|
1045
|
+
);
|
|
1046
|
+
println!(
|
|
1047
|
+
"Firefox Profiler ({}) for analysis.",
|
|
1048
|
+
"https://profiler.firefox.com/".underline().cyan()
|
|
1049
|
+
);
|
|
1050
|
+
println!();
|
|
1051
|
+
|
|
1052
|
+
let config = Config { license_key: None };
|
|
1053
|
+
|
|
1054
|
+
match std::fs::write(
|
|
1055
|
+
&config_path,
|
|
1056
|
+
toml::to_string_pretty(&config).wrap_err("Failed to serialize config file")?,
|
|
1057
|
+
) {
|
|
1058
|
+
Ok(_) => {
|
|
1059
|
+
println!(
|
|
1060
|
+
"{}{}",
|
|
1061
|
+
format_args!("Default config saved to {}", config_path.display().bold())
|
|
1062
|
+
.dimmed(),
|
|
1063
|
+
". This startup message will not be shown again.".dimmed()
|
|
1064
|
+
);
|
|
1065
|
+
println!();
|
|
1066
|
+
}
|
|
1067
|
+
Err(e) => {
|
|
1068
|
+
eprintln!(
|
|
1069
|
+
"Failed to create default FunctionTrace config file at {}: {e}",
|
|
1070
|
+
config_path.display().dimmed(),
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
};
|
|
1074
|
+
|
|
1075
|
+
config
|
|
1076
|
+
};
|
|
1077
|
+
|
|
1078
|
+
// We need to write out the config if it's changed. Track the new and old configs so we
|
|
1079
|
+
// can tell.
|
|
1080
|
+
let mut new_config = config.clone();
|
|
1081
|
+
|
|
1082
|
+
////////////////////////////////////////////////////////////////////////
|
|
1083
|
+
// Find their license key
|
|
1084
|
+
////////////////////////////////////////////////////////////////////////
|
|
1085
|
+
// Demo keys have the same general shape as a UUID, but include the expiration time
|
|
1086
|
+
const DEMO_KEY_PREFIX: &str = "TRIALKEYX-";
|
|
1087
|
+
const DEMO_KEY_FORMAT: &str = "%Y-%m%d-%H%M-%S%z";
|
|
1088
|
+
const DEMO_KEY_SUFFIX: &str = "TRIAL";
|
|
1089
|
+
|
|
1090
|
+
let license_key = if let Some(license_key) = &config.license_key {
|
|
1091
|
+
license_key.clone()
|
|
1092
|
+
} else {
|
|
1093
|
+
println!("FunctionTrace is open-source and may freely be used noncommercially under");
|
|
1094
|
+
println!(
|
|
1095
|
+
"the Prosperity Public License 3.0 ({}).",
|
|
1096
|
+
"https://prosperitylicense.com/".underline().cyan()
|
|
1097
|
+
);
|
|
1098
|
+
println!();
|
|
1099
|
+
|
|
1100
|
+
/// Print a prompt, then read lines of input repeatedly until it successfully parses.
|
|
1101
|
+
fn read_input<T, F>(prompt: std::fmt::Arguments, parser: F) -> Result<T>
|
|
1102
|
+
where
|
|
1103
|
+
F: Fn(&String) -> Option<T>,
|
|
1104
|
+
{
|
|
1105
|
+
let mut input = String::new();
|
|
1106
|
+
|
|
1107
|
+
loop {
|
|
1108
|
+
print!("{}", prompt);
|
|
1109
|
+
std::io::stdout().flush()?;
|
|
1110
|
+
|
|
1111
|
+
input.clear();
|
|
1112
|
+
std::io::stdin().read_line(&mut input)?;
|
|
1113
|
+
|
|
1114
|
+
if let Some(result) = parser(&input) {
|
|
1115
|
+
return Ok(result);
|
|
1116
|
+
}
|
|
1117
|
+
Python::with_gil(|py| {
|
|
1118
|
+
// We need to check with the Python interpreter during loops, otherwise we
|
|
1119
|
+
// might not receive ctrl-c events.
|
|
1120
|
+
py.check_signals()
|
|
1121
|
+
})?;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
let commercial = read_input(
|
|
1126
|
+
format_args!(
|
|
1127
|
+
"{} {}: ",
|
|
1128
|
+
"Are you using FunctionTrace commercially?".bold(),
|
|
1129
|
+
"[yes/no]".dimmed()
|
|
1130
|
+
),
|
|
1131
|
+
|input| match input.chars().next() {
|
|
1132
|
+
Some('y') => Some(true),
|
|
1133
|
+
Some('n') => Some(false),
|
|
1134
|
+
_ => {
|
|
1135
|
+
eprintln!(
|
|
1136
|
+
"Failed to read {} or {} input.",
|
|
1137
|
+
"yes".dimmed(),
|
|
1138
|
+
"no".dimmed()
|
|
1139
|
+
);
|
|
1140
|
+
None
|
|
1141
|
+
}
|
|
1142
|
+
},
|
|
1143
|
+
)?;
|
|
1144
|
+
|
|
1145
|
+
let key = if !commercial {
|
|
1146
|
+
"NONCOMMERCIAL".to_string()
|
|
1147
|
+
} else {
|
|
1148
|
+
println!();
|
|
1149
|
+
println!(
|
|
1150
|
+
"The Prosperity Public License allows for a single 30 day trial period for commercial users."
|
|
1151
|
+
);
|
|
1152
|
+
let demo = read_input(
|
|
1153
|
+
format_args!(
|
|
1154
|
+
"{} {}: ",
|
|
1155
|
+
"Is this your company's first time using FunctionTrace?".bold(),
|
|
1156
|
+
"[yes/no]".dimmed()
|
|
1157
|
+
),
|
|
1158
|
+
|input| match input.chars().next() {
|
|
1159
|
+
Some('y') => Some(true),
|
|
1160
|
+
Some('n') => Some(false),
|
|
1161
|
+
_ => {
|
|
1162
|
+
eprintln!(
|
|
1163
|
+
"Failed to read {} or {} input.",
|
|
1164
|
+
"yes".dimmed(),
|
|
1165
|
+
"no".dimmed()
|
|
1166
|
+
);
|
|
1167
|
+
None
|
|
1168
|
+
}
|
|
1169
|
+
},
|
|
1170
|
+
)?;
|
|
1171
|
+
|
|
1172
|
+
if demo {
|
|
1173
|
+
// They haven't used FunctionTrace yet, so we'll generate them a demo key
|
|
1174
|
+
// that's valid for 30 days.
|
|
1175
|
+
let expiration = Timestamp::now()
|
|
1176
|
+
.checked_add(Duration::from_secs(3600 * 24 * 30))
|
|
1177
|
+
.expect("30 days in the future is valid");
|
|
1178
|
+
|
|
1179
|
+
// Generate a key formatted like a UUID, but with the expiration time included
|
|
1180
|
+
format!(
|
|
1181
|
+
"{DEMO_KEY_PREFIX}{}{DEMO_KEY_SUFFIX}",
|
|
1182
|
+
expiration.strftime(DEMO_KEY_FORMAT)
|
|
1183
|
+
)
|
|
1184
|
+
} else {
|
|
1185
|
+
println!();
|
|
1186
|
+
println!(
|
|
1187
|
+
"Commercial users must purchase an alternative license via {}.",
|
|
1188
|
+
"https://functiontrace.com".underline().cyan()
|
|
1189
|
+
);
|
|
1190
|
+
|
|
1191
|
+
read_input(
|
|
1192
|
+
format_args!("{}: ", "Enter your FunctionTrace license key".bold()),
|
|
1193
|
+
|input| {
|
|
1194
|
+
if input.trim().chars().filter(|&c| c == '-').count() == 4 {
|
|
1195
|
+
// The key looks plausible. We'll validate it later.
|
|
1196
|
+
Some(input.trim().to_string())
|
|
1197
|
+
} else {
|
|
1198
|
+
eprintln!(
|
|
1199
|
+
"Invalid license format. FunctionTrace license keys look like {}.",
|
|
1200
|
+
"XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX".dimmed()
|
|
1201
|
+
);
|
|
1202
|
+
None
|
|
1203
|
+
}
|
|
1204
|
+
},
|
|
1205
|
+
)?
|
|
1206
|
+
}
|
|
1207
|
+
};
|
|
1208
|
+
|
|
1209
|
+
new_config.license_key = Some(key.clone());
|
|
1210
|
+
key
|
|
1211
|
+
};
|
|
1212
|
+
|
|
1213
|
+
////////////////////////////////////////////////////////////////////////
|
|
1214
|
+
// Validate the license key
|
|
1215
|
+
////////////////////////////////////////////////////////////////////////
|
|
1216
|
+
let valid_license = if license_key == "NONCOMMERCIAL" {
|
|
1217
|
+
// The user has asserted that they're not using FunctionTrace for commercial
|
|
1218
|
+
// purposes. Nothing left to validate here.
|
|
1219
|
+
true
|
|
1220
|
+
} else if let Some(demo_key) = license_key
|
|
1221
|
+
.strip_prefix(DEMO_KEY_PREFIX)
|
|
1222
|
+
.and_then(|s| s.strip_suffix(DEMO_KEY_SUFFIX))
|
|
1223
|
+
{
|
|
1224
|
+
// The user has a demo key that's valid for 30 days from creation.
|
|
1225
|
+
let now = Timestamp::now();
|
|
1226
|
+
|
|
1227
|
+
if let Ok(valid_expiration) = Timestamp::strptime(DEMO_KEY_FORMAT, demo_key) {
|
|
1228
|
+
if now
|
|
1229
|
+
.until(valid_expiration)
|
|
1230
|
+
.map_or(false, |remaining| remaining.is_positive())
|
|
1231
|
+
{
|
|
1232
|
+
// The license is still valid
|
|
1233
|
+
eprintln!(
|
|
1234
|
+
"{}: You're using a trial license that expires on {}. Please visit {} to acquire a full license.",
|
|
1235
|
+
"FunctionTrace trial".yellow().bold(),
|
|
1236
|
+
valid_expiration.strftime("%Y-%m-%d").dimmed(),
|
|
1237
|
+
"https://functiontrace.com".underline().cyan(),
|
|
1238
|
+
);
|
|
1239
|
+
true
|
|
1240
|
+
} else {
|
|
1241
|
+
eprintln!(
|
|
1242
|
+
"{}: You're using a trial license that expired on {}. Please visit {} to acquire a full license.",
|
|
1243
|
+
"FunctionTrace trial expired".red().bold(),
|
|
1244
|
+
valid_expiration.strftime("%Y-%m-%d").dimmed(),
|
|
1245
|
+
"https://functiontrace.com".underline().cyan(),
|
|
1246
|
+
);
|
|
1247
|
+
false
|
|
1248
|
+
}
|
|
1249
|
+
} else {
|
|
1250
|
+
// This demo key doesn't parse, so it's presumably invalid.
|
|
1251
|
+
eprintln!(
|
|
1252
|
+
"{}: your company's trial has expired. Please visit {} to acquire a license.",
|
|
1253
|
+
"FunctionTrace trial expired".red().bold().dimmed(),
|
|
1254
|
+
"https://functiontrace.com".underline().cyan(),
|
|
1255
|
+
);
|
|
1256
|
+
false
|
|
1257
|
+
}
|
|
1258
|
+
} else {
|
|
1259
|
+
// The user has a commercial license key. Validate it against our server
|
|
1260
|
+
let client = reqwest::blocking::Client::new();
|
|
1261
|
+
if let Ok(license_check) = client
|
|
1262
|
+
.get("https://license.functiontrace.com")
|
|
1263
|
+
.header("functiontrace-key", &license_key)
|
|
1264
|
+
.timeout(
|
|
1265
|
+
// We set a short timeout, since the license server should respond within a few
|
|
1266
|
+
// ms and we don't want to block users if it can't be contacted.
|
|
1267
|
+
Duration::from_secs(2),
|
|
1268
|
+
)
|
|
1269
|
+
.send()
|
|
1270
|
+
{
|
|
1271
|
+
/// Represents useful information about a given license
|
|
1272
|
+
#[derive(Deserialize)]
|
|
1273
|
+
struct LicenseInfo {
|
|
1274
|
+
#[serde(rename = "Seats")]
|
|
1275
|
+
seats: u32,
|
|
1276
|
+
#[serde(rename = "Expires")]
|
|
1277
|
+
expires: Timestamp,
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
match license_check.status() {
|
|
1281
|
+
reqwest::StatusCode::OK => {
|
|
1282
|
+
// This key is live!
|
|
1283
|
+
if let Ok(info) = license_check.json::<LicenseInfo>() {
|
|
1284
|
+
tracing::info!(
|
|
1285
|
+
seats = info.seats,
|
|
1286
|
+
expiration = info.expires.strftime("%F").to_string(),
|
|
1287
|
+
"Valid license loaded"
|
|
1288
|
+
);
|
|
1289
|
+
} else {
|
|
1290
|
+
tracing::info!(
|
|
1291
|
+
seats = "unknown",
|
|
1292
|
+
expiration = "unknown",
|
|
1293
|
+
"Valid license loaded"
|
|
1294
|
+
);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
reqwest::StatusCode::BAD_REQUEST => {
|
|
1298
|
+
// This license does not and has never existed.
|
|
1299
|
+
eprintln!(
|
|
1300
|
+
"{}: {} is not a valid FunctionTrace license.",
|
|
1301
|
+
"Invalid Functiontrace license".red().bold(),
|
|
1302
|
+
license_key.dimmed()
|
|
1303
|
+
);
|
|
1304
|
+
|
|
1305
|
+
// Mark this license key for deletion. We don't need to inform the user,
|
|
1306
|
+
// since it never could have worked.
|
|
1307
|
+
new_config.license_key = None;
|
|
1308
|
+
}
|
|
1309
|
+
reqwest::StatusCode::PAYMENT_REQUIRED => {
|
|
1310
|
+
// This license used to be valid but isn't anymore.
|
|
1311
|
+
if let Ok(info) = license_check.json::<LicenseInfo>() {
|
|
1312
|
+
eprintln!(
|
|
1313
|
+
"{}: {} expired on {}. Please visit {} to acquire a new license.",
|
|
1314
|
+
"FunctionTrace license expired".red().bold(),
|
|
1315
|
+
license_key.dimmed(),
|
|
1316
|
+
info.expires.strftime("%F").bold(),
|
|
1317
|
+
"https://functiontrace.com".underline().cyan(),
|
|
1318
|
+
);
|
|
1319
|
+
} else {
|
|
1320
|
+
eprintln!(
|
|
1321
|
+
"{}: {} has expired. Please visit {} to acquire a new license.",
|
|
1322
|
+
"FunctionTrace license expired".red().bold(),
|
|
1323
|
+
license_key.dimmed(),
|
|
1324
|
+
"https://functiontrace.com".underline().cyan(),
|
|
1325
|
+
);
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
eprintln!(
|
|
1329
|
+
"Removing expired FunctionTrace license key from {}.",
|
|
1330
|
+
config_path.display().dimmed()
|
|
1331
|
+
);
|
|
1332
|
+
new_config.license_key = None;
|
|
1333
|
+
}
|
|
1334
|
+
reqwest::StatusCode::INTERNAL_SERVER_ERROR => {
|
|
1335
|
+
// The license server crashed?
|
|
1336
|
+
tracing::warn!(
|
|
1337
|
+
"License server returned an error - assuming license is valid"
|
|
1338
|
+
);
|
|
1339
|
+
}
|
|
1340
|
+
status => {
|
|
1341
|
+
tracing::warn!(
|
|
1342
|
+
code = status.as_u16(),
|
|
1343
|
+
"License server returned an unexpected code - assuming license is valid"
|
|
1344
|
+
);
|
|
1345
|
+
}
|
|
1346
|
+
};
|
|
1347
|
+
|
|
1348
|
+
// If there's still a license key stored in the config file, we must've validated
|
|
1349
|
+
// it.
|
|
1350
|
+
new_config.license_key.is_some()
|
|
1351
|
+
} else {
|
|
1352
|
+
// We didn't successfully talk to the license server. Assume everything is fine.
|
|
1353
|
+
true
|
|
1354
|
+
}
|
|
1355
|
+
};
|
|
1356
|
+
|
|
1357
|
+
////////////////////////////////////////////////////////////////////////
|
|
1358
|
+
// Save the updated config
|
|
1359
|
+
////////////////////////////////////////////////////////////////////////
|
|
1360
|
+
if new_config != config {
|
|
1361
|
+
// We updated the configuration file, so save it out.
|
|
1362
|
+
if let Err(e) = std::fs::write(
|
|
1363
|
+
&config_path,
|
|
1364
|
+
toml::to_string_pretty(&new_config).wrap_err("Failed to serialize config file")?,
|
|
1365
|
+
) {
|
|
1366
|
+
eprintln!(
|
|
1367
|
+
"Failed to update FunctionTrace config file at {}: {e}",
|
|
1368
|
+
config_path.display().dimmed(),
|
|
1369
|
+
);
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
if !valid_license {
|
|
1374
|
+
// We've already printed information about what's happening, so it's safe to just kill
|
|
1375
|
+
// the process.
|
|
1376
|
+
std::process::exit(1);
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
Ok(())
|
|
1380
|
+
}
|
|
891
1381
|
}
|
|
892
1382
|
|
|
893
1383
|
/// Allocation hooks and helpers for tracing Python memory allocator operations.
|
|
@@ -997,7 +1487,7 @@ mod hooks {
|
|
|
997
1487
|
use color_eyre::eyre::{Result, WrapErr};
|
|
998
1488
|
use functiontrace_server::function_trace;
|
|
999
1489
|
use pyo3::ffi::PyObject;
|
|
1000
|
-
use pyo3::prelude::*;
|
|
1490
|
+
use pyo3::prelude::*;
|
|
1001
1491
|
use serde::Serialize;
|
|
1002
1492
|
use std::borrow::Cow;
|
|
1003
1493
|
use std::sync::OnceLock;
|
|
@@ -1148,7 +1638,7 @@ mod hooks {
|
|
|
1148
1638
|
.get()
|
|
1149
1639
|
.expect("Wrapper functions can't be called until hooked");
|
|
1150
1640
|
|
|
1151
|
-
Python::with_gil(|py| -> Result<()> {
|
|
1641
|
+
let proxied = Python::with_gil(|py| -> Result<()> {
|
|
1152
1642
|
if let Some(mut writer) = crate::_functiontrace_rs::thread_state() {
|
|
1153
1643
|
let args = unsafe {
|
|
1154
1644
|
// SAFETY: `args` represents a valid argument tuple
|
|
@@ -1167,10 +1657,15 @@ mod hooks {
|
|
|
1167
1657
|
|
|
1168
1658
|
Ok(())
|
|
1169
1659
|
})
|
|
1170
|
-
.wrap_err_with(|| format!("FunctionTrace proxy logging for {} failed", hook.target))
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1660
|
+
.wrap_err_with(|| format!("FunctionTrace proxy logging for {} failed", hook.target));
|
|
1661
|
+
|
|
1662
|
+
if let Err(e) = proxied {
|
|
1663
|
+
tracing::error!(
|
|
1664
|
+
error = e.to_string(),
|
|
1665
|
+
function = hook.target,
|
|
1666
|
+
"Failed to properly handle hooked function"
|
|
1667
|
+
);
|
|
1668
|
+
}
|
|
1174
1669
|
|
|
1175
1670
|
unsafe {
|
|
1176
1671
|
// SAFETY: This is a direct translation of the original call we're proxying
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|