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.
@@ -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.8", default-features = false, features = ["parse"] }
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,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: functiontrace
3
- Version: 0.4.4
3
+ Version: 0.5.0
4
4
  Summary: A profiling tool that traces your application's execution
5
5
  Author: Alistair Dobke
6
6
  Author-email: Matt Bryant <functiontrace@programsareproofs.com>
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: functiontrace
3
- Version: 0.4.4
3
+ Version: 0.5.0
4
4
  Summary: A profiling tool that traces your application's execution
5
5
  Author: Alistair Dobke
6
6
  Author-email: Matt Bryant <functiontrace@programsareproofs.com>
@@ -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,6 +1,6 @@
1
1
  [project]
2
2
  name = "functiontrace"
3
- version = "0.4.4"
3
+ version = "0.5.0"
4
4
  description = "A profiling tool that traces your application's execution"
5
5
  authors = [
6
6
  { name = "Matt Bryant", email = "functiontrace@programsareproofs.com"},
@@ -1,12 +1,13 @@
1
1
  #[pyo3::pymodule]
2
2
  mod _functiontrace_rs {
3
- use color_eyre::eyre::{eyre, OptionExt, Result, WrapErr};
3
+ use color_eyre::eyre::{OptionExt, Result, WrapErr, eyre};
4
4
  use functiontrace_server::function_trace;
5
- use pyo3::prelude::*; // TODO: What's in here?
6
- use serde::Serialize;
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::{c_void, CStr, OsString};
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 of the data, so reset our buffer. Do this now to ensure we
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
- .wrap_err("Failed to launch functiontrace-server")?;
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
- // TODO: tracing log that this happened
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
- // TODO: tracing log that this happened
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 of the current allocators.
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
- // TODO: tracing log this
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
- // TODO: this shouldn't be possible - tracing log it so we can handle later.
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::*; // TODO: What's in here?
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
- // TODO: This shouldn't be catastrophic - we can log it and continue on
1172
- // instead once we introduce `tracing`.
1173
- .expect("Proxying should never fail");
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